Причина ошибки переполнения деления (x86)

у меня есть несколько вопросов об ошибках переполнения divide в архитектуре x86 или x86_64. В последнее время я читал о целочисленных переполнений. Обычно, когда арифметическая операция приводит к переполнению целого числа, устанавливается бит переноса или бит переполнения в регистре флагов. Но, по-видимому, согласно в этой статье, переполнения в результате операций деления не устанавливают бит переполнения, а скорее вызывают аппаратное исключение, подобное тому, когда вы делите на ноль.

теперь целочисленные переполнения в результате деления намного реже, чем, скажем, умножение. Есть только несколько способов, чтобы вызвать переполнение деления. Одним из способов было бы сделать что-то вроде:

int16_t a = -32768;
int16_t b = -1;
int16_t c = a / b;

в этом случае из-за того, что два дополняют представление целых чисел со знаком, вы не можете представить положительный 32768 в 16-разрядном целочисленном со знаком, поэтому операция деления переполняется, что приводит к ошибочному значению -32768.

несколько вопросы:

1) вопреки тому, что говорится в этой статье, вышеизложенное не вызвало аппаратного исключения. Я использую машину x86_64 под управлением Linux, и когда я делю на ноль, программа завершается с Floating point exception. Но когда я вызываю переполнение деления, программа продолжает, как обычно, молча игнорируя ошибочный фактор. Так почему же это не вызывает аппаратного исключения?

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

=========== редактировать ==============

Ладно, спасибо всем за ответы. Я получил ответы, говорящие в основном, что вышеуказанное 16-битное целочисленное деление не должно вызывать аппаратную ошибку, потому что фактор все еще меньше, чем размер регистра. Я этого не понимаю. В этом случае регистр, хранящий фактор, равен 16-битному-это слишком маленький для сохранения положительных 32768. Так почему же не возникает исключение оборудования?

Хорошо, давайте сделаем это непосредственно в встроенной сборке GCC и посмотрим, что произойдет:

int16_t a = -32768;
int16_t b = -1;

__asm__
(
    "xorw %%dx, %%dx;"            // Clear the DX register (upper-bits of dividend)
    "movw %1, %%ax;"              // Load lower bits of dividend into AX
    "movw %2, %%bx;"              // Load the divisor into BX
    "idivw %%bx;"                 // Divide a / b (quotient is stored in AX)
    "movw %%ax, %0;"              // Copy the quotient into 'b'
    : "=rm"(b)                    // Output list
    :"ir"(a), "rm"(b)             // Input list
    :"%ax", "%dx", "%bx"          // Clobbered registers
);

printf("%dn", b);

Это просто выводит ошибочное значение:-32768. По-прежнему нет аппаратного исключения, хотя регистр, хранящий фактор (AX), слишком мал, чтобы соответствовать коэффициент. Поэтому я не понимаю, почему здесь нет аппаратной ошибки.

7 ответов


в языке C арифметические операции никогда не выполняются в пределах типов меньше int. Каждый раз, когда вы пытаетесь арифметику на меньших операндах, они сначала подвергаются интегральным промо-акциям, которые преобразуют их в int. Если на вашей платформе int, скажем, 32-битный, тогда нет способа заставить программу C выполнять 16-битное деление. Вместо этого компилятор будет генерировать 32-разрядное разделение. Вероятно, именно поэтому ваш эксперимент C не вызывает ожидаемого переполнения деление. Если ваша платформа действительно имеет 32-битный int, то лучше всего было бы попробовать то же самое с 32-битными операндами (т. е. divide INT_MIN by -1). Я уверен, что таким образом Вы сможете в конечном итоге воспроизвести исключение переполнения даже в коде C.


в вашем коде сборки вы используете 16-битное деление, так как вы указали BX как операнд для idiv. 16-битное деление на x86 делит 32-битный дивиденд, хранящийся в DX:AX пара idiv операнд. Это то, что вы делаете в коде. The DX:AX пара интерпретируется как один составной 32-разрядный регистр, что означает, что бит знака в этой паре теперь фактически является битом высшего порядка DX. Бит высшего порядка AX это больше не знак.

и что вы сделали с DX? Вы просто очистили его. Вы установите его в 0. Но с DX установите значение 0, ваш дивиденд интерпретируется как положительное! С точки зрения, такой DX:AX пара фактически представляет собой положительное стоимостью +32768. Т. е. в своем эксперименте на ассемблере вы делите +32768 by -1. И результат -32768, как следует. Ничего необычного.

если вы хотите представлять -32768 на DX:AX пара, вы должны подписать-расширить его, т. е. вы должны заполнить DX с шаблоном все-один бит вместо нулей. Вместо того, чтобы делать xor DX, DX вы должны инициализировать AX С -32768 а потом cwd. Что бы знак AX на DX.

например, в моем эксперименте (не GCC) этот код

__asm  {
  mov AX, -32768
  cwd
  mov BX, -1
  idiv BX
}

вызывает ожидаемое исключение, потому что он действительно пытается разделить -32768 by -1.


когда вы получаете переполнение integer с дополнением integer 2 add / subtract / multiply, у вас все еще есть действительный результат - просто не хватает некоторых битов высокого порядка. Такое поведение часто полезно, поэтому было бы нецелесообразно создавать исключение для этого.

с целочисленным делением, однако результат деления на ноль бесполезен (так как, в отличие от с плавающей запятой, целые числа дополнения 2 не имеют представления INF).


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

в статье этого не сказано. Это говорит

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

размер регистра определенно больше, чем 16 бит (32 || 64)


из соответствующего раздела integer overflow:

В отличие от add, mul и imul инструкции, подразделение Intel инструкции div и исключением idiv не установлен флаг переполнения; они генерируют ошибка деления, если исходный операнд (делитель) равен нулю или частное слишком большой для места реестр.

размер регистра на современной платформе или 32 или 64 бита; 32768 поместится в один из тех реестры. Однако следующий код, скорее всего, вызовет целочисленное переполнение (это происходит на моем ноутбуке core Duo на VC8):

int x= INT_MIN;
int y= -1;
int z= x/y;

  1. причина, по которой ваш пример не генерировал аппаратное исключение, связана с целочисленными правилами продвижения C. Операнды меньше int вам автоматически повышен до ints перед выполнением операции.

  2. что касается того, почему разные виды переполнений обрабатываются по-разному, подумайте, что на уровне машины x86 действительно нет такого переполнения умножения. Когда вы умножаете AX на какой-либо другой регистр, результат переходит в DX: AX пара, поэтому всегда есть место для результата, и, следовательно, нет повода сигнализировать об исключении переполнения. Однако, в C и других языках, произведение двух ints должен вписываться в int, значит, есть такая вещь, как переполнение на уровне C. X86 иногда устанавливает (флаг переполнения) на MULs, но это просто означает, что высокая часть результата ненулевая.


на реализацию, с 32-битной int ваш пример не приводит к переполнению divide. Это приводит к совершенно представимым int, 32768, который затем преобразуется в int16_t в определенной реализацией манере, когда вы делаете назначение. Это связано с промо-акциями по умолчанию, указанными языком C, и в результате реализация, которая вызвала исключение здесь, была бы несоответствующей.

если вы хотите попытаться вызвать исключение (что все еще может или не может произойти, это зависит от реализации), попробуйте:

int a = INT_MIN, b = -1, c = a/b;

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


Я бы предположил, что на некоторых старых компьютерах попытка деления на ноль вызовет некоторые серьезные проблемы (например, поместить оборудование в бесконечный цикл попыток вычесть достаточно, чтобы остаток был меньше дивиденда, пока оператор не придет, чтобы исправить вещи), и это начало традицию деления переполнений, рассматриваемых как более серьезные ошибки, чем целочисленные переполнения.

с точки зрения программирования нет причин, по которым неожиданное переполнение divide должно быть более или менее серьезным, чем неожиданное переполнение целого числа (со знаком или без знака). Учитывая стоимость разделения, предельная стоимость проверки флага переполнения после этого будет довольно незначительной. Традиция-единственная причина, по которой я вижу аппаратную ловушку.