На каких платформах целочисленное деление на ноль вызывает исключение с плавающей запятой?

В другом вопросе кто-то задавался вопросом, почему они получают "ошибку с плавающей запятой", когда на самом деле они имеют целочисленное деление на ноль в своей программе на C++. Вокруг этого возникла дискуссия, в которой некоторые утверждали, что исключения с плавающей запятой фактически никогда не возникают при делении с плавающей запятой на ноль, а возникают только при делении целого числа на ноль.

Это звучит странно для меня, потому что я знаю, что:

  1. Скомпилированный в MSVC код на x86 и x64 на всех платформах Windows сообщает о делении int на ноль как "0xc0000094: деление на целое число на ноль", а делении с плавающей точкой на ноль - на 0xC000008E "Деление с плавающей точкой на ноль" (когда включено)

  2. ISA -32 и AMD64 определяют #DE (исключение целочисленного деления) как прерывание 0. Исключения с плавающей точкой вызывают прерывание 16 (x87 с плавающей точкой) или прерывание 19 (SIMD с плавающей точкой).

  3. У другого оборудования аналогично разные прерывания (например, PPC вызывает 0x7000 при float-div-by-zero и вообще не перехватывает int/0).

  4. Наше приложение разоблачает исключения с плавающей точкой для деления на ноль с помощью _controlfp_s свойственный (в конечном итоге stmxcsr op), а затем ловит их для целей отладки. Так что на практике я определенно видел исключения деления на ноль в IEEE754.

Таким образом, я заключаю, что есть некоторые платформы, которые сообщают об исключениях int как об исключениях с плавающей запятой, таких как x64 Linux (повышение SIGFPE для всех арифметических ошибок независимо от канала ALU).

Какие другие операционные системы (или среды выполнения C/C++, если вы операционная система) сообщают целое число, деленное на ноль, как исключение с плавающей запятой?

3 ответа

Я не уверен, как сложилась текущая ситуация, но в настоящее время поддержка обнаружения исключений в FP сильно отличается от целочисленной. Целочисленное деление обычно в ловушку. POSIX требует его поднять SIGFPE если это вызывает исключение вообще.

Тем не менее, вы можете разобраться, что это был за SIGFPE, чтобы увидеть, что это на самом деле исключение деления. (Не обязательно делить на ноль, хотя: дополнение 2 INT_MIN / -1 ловушки деления и х86 div а также idiv также ловушка, когда частное деления 64b/32b не помещается в регистр вывода 32b.)

В руководстве по glibc объясняется, что системы BSD и GNU доставляют дополнительный аргумент обработчику сигнала для SIGFPE, которые будут FPE_INTDIV_TRAP для деления на ноль. Документы POSIX FPE_INTDIV_TRAP в качестве возможного значения для siginfo_t "s int si_code поле, в системах, где siginfo_t включает в себя этот член.

IDK, если Windows доставляет другое исключение в первую очередь, или если она объединяет вещи в разные разновидности одного и того же арифметического исключения, как в Unix. Если это так, обработчик по умолчанию декодирует дополнительную информацию, чтобы сообщить вам, какое это было исключение.

POSIX и Windows используют фразу "деление на ноль", чтобы охватить все исключения целочисленного деления, так что, очевидно, это обычное сокращение. Для людей, которые знают, что INT_MIN / -1 (с дополнением 2) является проблемой, фразу "деление на ноль" можно считать синонимом исключения деления. Фраза сразу указывает на общий случай для людей, которые не знают, почему целочисленное деление может быть проблемой.


Семантика исключений FP

Исключения FP по умолчанию маскируются для процессов пользовательского пространства в большинстве операционных систем / C ABI.

Это имеет смысл, потому что IEEE с плавающей запятой может представлять бесконечности и имеет NaN для распространения ошибки на все будущие вычисления с использованием значения.

  • 0.0/0.0 => NaN
  • Если x конечно: x/0.0 => +/-Inf со знаком х

Это даже позволяет таким вещам давать ощутимый результат, когда исключения маскируются:

double x = 0.0;
double y = 1.0/x;   // y = +Inf
double z = 1.0/y;   // z = 1/Inf = 0.0, no FP exception

FP против целочисленного обнаружения ошибок

Способ обнаружения ошибок в FP очень хорош: когда маскируются исключения, они устанавливают флаг в регистре состояния FP вместо перехвата. (например, MXCSR x86 для инструкций SSE). Флаг остается установленным до тех пор, пока не будет сброшен вручную, поэтому вы можете проверить один раз (например, после цикла), чтобы увидеть, какие исключения произошли, а не где они произошли.

Были предложения о том, чтобы иметь аналогичные "липкие" флаги целочисленного переполнения для записи, если переполнение произошло в любой точке во время последовательности вычислений. Позволять маскировать исключения целочисленного деления было бы неплохо в некоторых случаях, но опасно в других случаях (например, при вычислении адреса вы должны перехватывать вместо того, чтобы потенциально хранить в поддельном месте).

На x86, однако, обнаружение переполнения целого числа во время последовательности вычислений требует установки условной ветви после каждого из них, поскольку флаги просто перезаписываются. MIPS имеет add инструкция, которая будет ловить при переполнении со знаком, и команда без знака, которая никогда не перехватывает. Таким образом, обнаружение и обработка целочисленных исключений намного менее стандартизированы.


Целочисленное деление не позволяет выводить результаты NaN или Inf, поэтому имеет смысл работать таким образом.

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

Однако в x86 преобразование значения с плавающей запятой вне диапазона в целое число cvtsd2si или любая подобная инструкция преобразования производит "неопределенное целочисленное" значение, если исключение "неверное число с плавающей точкой" замаскировано. Значение равно нулю, кроме знакового бита. т.е. INT_MIN,

(См. Руководства Intel, ссылки в теге x86 вики.

Какие другие операционные системы (или среды выполнения C/C++, если вы операционная система) сообщают целое число, деленное на ноль, как исключение с плавающей запятой?

Ответ зависит от того, находитесь ли вы в пространстве ядра или в пространстве пользователя. Если вы находитесь в пространстве ядра, вы можете поместить "i / 0" в kernel_main(), пусть ваш обработчик прерываний вызовет обработчик исключений и остановит ваше ядро. Если вы находитесь в пространстве пользователя, ответ зависит от вашей операционной системы и настроек компилятора.

Аппаратное обеспечение AMD64 определяет целочисленное деление на ноль как прерывание 0, отличающееся от прерывания 16 (исключение с плавающей запятой x87) и прерывания 19 (исключение с плавающей запятой SIMD).

Исключение "деление на ноль" предназначено для деления на ноль с div инструкция. Обсуждение FPU x87 выходит за рамки этого вопроса.

Другое оборудование имеет аналогично различные прерывания (например, PPC поднимает 0x7000 при float-div-by-zero и вообще не перехватывает int/0).

Более конкретно, 00700 сопоставляется с типом исключения "Программа", который включает исключение с плавающей точкой. Такое исключение возникает при попытке деления на ноль с помощью инструкции с плавающей запятой.

С другой стороны, целочисленное деление является неопределенным поведением для PEM PPC:

8-53 divw

Если предпринята попытка выполнить любое из делений - 0x8000_0000 ÷ –1 или ÷ 0, то содержимое rD не определено, как и содержимое битов LT, GT и EQ поля CR0 (если Rc = 1). В этом случае, если OE = 1, тогда устанавливается OV.

Наше приложение разоблачает исключения с плавающей точкой для деления на ноль с помощью _controlfp_s intrinsic (в конечном счете, stmxcsr op), а затем перехватывает их в целях отладки. Так что на практике я определенно видел исключения деления на ноль в IEEE754.

Я думаю, что ваше время лучше потратить на разделение на ноль во время компиляции, а не во время выполнения.

Для пользователей это происходит в AIX, работающей на POWER, HP-UX на PA-RISC, Linux на x86-64, macOS на x86-64, Tru64 на Alpha и Solaris на SPARC.

Избегать деления на ноль во время компиляции намного лучше.

Другие вопросы по тегам