Вызывает ошибку переполнения деления (x86) - PullRequest
7 голосов
/ 08 октября 2010

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

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

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("%d\n", b);

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

Ответы [ 7 ]

13 голосов
/ 08 октября 2010

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


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

А что ты делал с DX? Вы просто очистили это. Вы устанавливаете его на 0. Но с DX, установленным на 0, ваш дивиденд интерпретируется как положительный ! С точки зрения машины такая пара DX:AX фактически представляет положительное значение +32768. То есть в вашем эксперименте на ассемблере вы делите +32768 на -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 на -1.

2 голосов
/ 08 октября 2010

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

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

1 голос
/ 08 октября 2010

Из соответствующего раздела о целочисленном переполнении :

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

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

int x= INT_MIN;
int y= -1;
int z= x/y;
1 голос
/ 08 октября 2010

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

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

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

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

0 голосов
/ 08 октября 2010

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

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

0 голосов
/ 08 октября 2010

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

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

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

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

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

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

...