Почему компилятор для неисполняемого кода иногда использует ud2, а иногда int3

Существует два распространённых способа, которыми компиляторы для x86 обозначают, что выполнение не должно было достигать определённой точки: один — однобайтовая опкод int 3 для точек останова, другой — двухбайтовая ud2 как недопустимая инструкция. Как они решают, какую использовать?

Эти два типа «неправильных инструкций» обычно применяются для разных целей.

Назначение int 3 и ud2

int 3 означает «Здесь вообще нет кода. Если вы каким-то образом сюда попали, значит кто-то использовал неверный указатель на функцию». Его используют как заполнение, например, между функциями. Нормальным путём исполнения код не может попасть на int 3. Должен был быть сгенерирован неверный адрес и выполнен вызов по нему.

ud2 используется для пометки случая, когда выполнение дошло до места, которое должно быть недостижимым. Это значит «Вы выполнили путь исполнения, который стандарт определяет как неопределённое поведение». Например, если управление падает с конца функции, возвращающей значение, без явного return, или если за вызовом помеченной [[noreturn]] функции вдруг последовал возврат.

Причины выбора между int 3 и ud2

Использование int 3 для «здесь даже нет кода» важно, потому что это однобайтовая инструкция. Если бы использовали двухбайтовый ud2, тогда неудачный указатель на функцию мог бы попасть на второй байт инструкции; в этом случае это уже не ud2. Вместо немедленной остановки началось бы исполнение мусорного кода.

Итак, что это значит для вас?

Если вы обнаружили, что исполняется ud2, ищите логические ошибки в своём коде. Если же исполняется int 3, ищите неинициализированную переменную с указателем на функцию, жёстко закодированную точку останова или точку останова, вставленную отладчиком.

Исторический контекст

Немного истории помогает понять это лучше.

У 8086/8088 не было трепа по недопустимой инструкции, поэтому невозможно было обнаружить ud2 на этих процессорах. INT3 был определён с самого начала и предназначался для «входа в отладчик»; идея была в том, чтобы система могла предоставить способы (программа в ОЗУ, специальная обработка адресации и т.п.), позволяющие отладчику заменить первый байт любой инструкции на INT3 как точку останова.

Поколение 80186/80286 добавило исключение «недопустимая инструкция» и изменило дизайн так, чтобы все инструкции имели определённое поведение — любое, что ещё не было определено осмысленным образом, стало вызывать исключение недопустимой инструкции. Затем Intel осознала (некоторое время до выпуска Pentium), что неявное оставление опкодов не определёнными как «вызывать исключение» создаёт проблемы, потому что программное обеспечение использовало пространство опкодов, которое Intel хотела оставить для расширения набора инструкций x86, просто чтобы вызывать это исключение. В результате они решили явно определить ud2 как инструкцию «вызывать исключение недопустимой инструкции», и были вынуждены ретроспективно определить ud0 и ud1 как инструкции, вызывающие то же исключение (хотя при этом возникли проблемы: некоторые прошлые процессоры декодировали ud0 и ud1 как двухбайтовые инструкции, а другие — предполагая, что за ними следует байт ModR/M и далее то, что указывал ModR/M).

После появления двух определённых способов для создания ловушки программное обеспечение стало использовать их для разных целей, как описано выше.

Заключение

Если вас интересует, есть ли нюансы, связанные с отладчиком, или случаи, когда нужно различать эти две инструкции: да — они исторически и семантически различаются, и отладчик/разработчик обычно ожидает, что int 3 — это точка останова (включая точки останова, вставленные отладчиком), а ud2 — признак реализованной недостижимой ветки или неопределённого поведения, которое следует расследовать в коде. По умолчанию их обработка и намерения использования различаются именно по этой причине.