Я объяснил оба случая ... второй дает вам деление на ноль в показанных помещениях ... вы меня опровергли? Вот почему вы получаете деление на ноль ..... :) понизить меня ... но ответ был там ....

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

Выводы кода 1-2147483648

int a = 0x80000000;
int b = a / -1;
printf("%d\n", b);

Фрагмент кода 2 ничего не выводит и выдаетFloating point exception

int a = 0x80000000;
int b = -1;
int c = a / b;
printf("%d\n", c);

Я хорошо знаю причину результата Code Snippet 1 (1 + ~INT_MIN == INT_MIN), но я не совсем понимаю, как целочисленное деление на -1 может генерировать FPE, и при этом я не могу воспроизвести его на своем телефоне Android (AArch64, GCC 7.2.0). Код 2 просто выводит то же, что и Код 1, без каких-либо исключений. Это скрытыйошибка особенность процессора x86?

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

редактироватьЯ связался с моим другом, и он протестировал код на Ubuntu 16.04 (Intel Kaby Lake, GCC 6.3.0). Результат соответствовал тому, что указано в назначении (код 1 выдает указанную вещь, а код 2 аварийно завершает работу с FPE).

 iBug23 сент. 2017 г., 11:45
@paxdiablo Отредактировал вопрос. Это не UB, а поведение процессора, зависящее от архитектуры.
 iBug23 сент. 2017 г., 12:09
@ T.C. Ваш комментарий является правильным ответом.
 Basile Starynkevitch23 сент. 2017 г., 11:46
@iBug: в этом вопросе следует упомянуть архитектуру (то есть речь идет не о C, а о машинном коде, полученном каким-то конкретным компилятором C на определенной машине). На x86-64 целочисленные переполнения в Linux могут датьSIGFPE
 paxdiablo23 сент. 2017 г., 11:43
«У меня есть назначение объяснения некоторых UBs C» - ошибка. Единственный способ объяснить это - сказать абсолютночто-нибудь может случиться.
 T.C.23 сент. 2017 г., 11:59
Я, конечно, не удивлюсь, если ваш первый фрагмент будет составлен с отрицанием, а не с разделением. Это простая оптимизация глазка.

Ответы на вопрос(5)

фактически используя IDIV операция (которая на самом деле не нужна для постоянных аргументов, даже для переменных, которые известны как постоянные, но это все равно происходило),INT_MIN / -1 это один из случаев, который приводит к #DE (ошибка деления). Это действительно частный случай, когда частное находится вне диапазона, в общем, это возможно, потому чтоidiv делит очень широкий дивиденд на делитель, поэтому многие комбинации вызывают переполнение - ноINT_MIN / -1 это единственный случай, который не является делителем на 0, к которому вы обычно можете обращаться из языков более высокого уровня, поскольку они, как правило, не раскрывают возможности сверхширокого дивиденда.

Linux досадно отображает #DE в SIGFPE, что, вероятно, смутило всех, кто имел дело с этим в первый раз.

 Peter Cordes23 сент. 2017 г., 15:24
POSIX требуетSIGFPE если есть сигнал вообще. : / Но в системах Linux / GNU, видимо, можно различитьFPE_INTDIV_TRAP и фактические причины FP:gnu.org/software/libc/manual/html_node/..., Смотрите мой ответ для объяснения почемуidiv необходимо сgcc -O0 для одного, но не для обоих случаев.

int деление на два дополнения не определено, если:

делитель ноль, ИЛИдивидендINT_MIN (==0x80000000 еслиint являетсяint32_t) и делитель-1 (в дополнение к двум,-INT_MIN > INT_MAX, что вызывает целочисленное переполнение, что является неопределенным поведением в C)

(https://www.securecoding.cert.org рекомендует переносить целочисленные операции в функции, которые проверяют такие крайние случаи)

Поскольку вы вызываете неопределенное поведение, нарушая правило 2, все может произойти, и когда это происходит, это что-то конкретное на вашей платформе оказывается сигналом FPE, генерируемым вашим процессором.

неопределенное поведение оченьПлохо вещи могут случиться, и иногда они случаются.

Ваш вопрос не имеет смысла в C (читатьЛаттнер на UB). Но вы можете получить код ассемблера (например, произведенныйgcc -O -fverbose-asm -S) и заботиться о поведении машинного кода.

На x86-64 с Linux целочисленное переполнение (а также целочисленное деление на ноль, IIRC) даетSIGFPE сигнал. ВидетьСигнал (7)

Кстати, по PowerPC целочисленное деление на ноль, по слухам, дает -1 на уровне машины (но некоторые компиляторы C генерируют дополнительный код для проверки этого случая).

Код в вашем вопросе - неопределенное поведение в C. Сгенерированный ассемблерный код имеет определенное поведение (зависит отЭТО и процессор).

(задание сделано, чтобы вы узнали больше о UB, особенноБлог Латтнера, который вы должныабсолютно читать)

 Peter Cordes23 сент. 2017 г., 13:43
Толькоgcc -O0 объясняет наблюдаемое поведение Что-нибудь кроме-O0 будет делать постоянное распространение во время компиляции. Но-O0 должен создать код, который все еще реализует исходный код C, если вы изменяете значения переменных во время выполнения с помощью отладчика. например он не может оптимизировать перезагрузку и x86idiv во 2-й версии, потому чтоint b = -1; не может быть до сих пор-1 в следующем заявлении.
 Peter Cordes23 сент. 2017 г., 15:24
Написал это в ответ.
 Peter Cordes23 сент. 2017 г., 13:46
Только целочисленное переполнениеот подразделения поднимает SIGFPE на x86-64 Linux. (На самом деле ИДК, что произойдет, если вы разбрасываетеinto инструкции после подписанного сложения / вычитания / чего угодно;#OF исключение может быть обработано так же, как#DE Исключением является ядро.) Но компилятор этого не делает, поэтому деление - единственная целочисленная операция, которая когда-либо перехватывается в C на x86-64.

-2147483648 по-1 и должен дать2147483648, а не результат, который вы получаете.

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

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

В частности,0x80000000 как представлено в дополнении два

1000_0000_0000_0000_0000_0000_0000

если мы дополним это число, то получим (сначала дополняем все биты, затем добавляем один)

0111_1111_1111_1111_1111_1111_1111 + 1 =>
1000_0000_0000_0000_0000_0000_0000  !!!  the same original number.

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

1000_0000_0000_0000_0000_0000_0000 &
0111_1111_1111_1111_1111_1111_1111 =>
0000_0000_0000_0000_0000_0000_0000

это число, которое вы используете в качестве делителя, что приводит к делению на ноль исключений.

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

ПРИМЕЧАНИЕ 1

Что касается компилятора, и стандарт ничего не говорит о допустимых диапазонахint это должно быть реализовано (стандарт обычно не включает0x8000...000 в двух дополнительных архитектурах) правильное поведение0x800...000 в двух архитектурах дополнения должно быть, так как оно имеет наибольшее абсолютное значение для целого числа этого типа, чтобы дать результат0 при делении числа на него. Но аппаратные реализации обычно не позволяют делить на такое число (так как многие из них даже не реализуют целочисленное деление со знаком, но имитируют его из деления без знака, поэтому многие просто извлекают знаки и делят деление без знака). Это требует проверить перед делением, и как говорит стандартНеопределенное поведениереализациям разрешено свободно избегать такой проверки и запрещать деление на это число. Они просто выбирают целочисленный диапазон0x8000...001 в0xffff...fffа затем из0x000..0000 в0x7fff...ffff, отвергая значение0x8000...0000 как недействительный

 Luis Colorado25 сент. 2017 г., 12:14
@iBug, в первом случае вы согласитесь со мной, что эквивалентно изменению знака на-2147483648 а этоневозможно (переполняется, что опять-таки U.B), потому что2147483648 являетсяне вint ассортимент.
 iBug25 сент. 2017 г., 12:15
я знаю1 + ~INT_MIN = INT_MIN.
 Luis Colorado25 сент. 2017 г., 12:16
Итак, какой ответ вы ищете?
 iBug25 сент. 2017 г., 11:59
Я сказал, что могу хорошо объяснить первый случай, так что это не тот ответ, который я ищу.
 Luis Colorado25 сент. 2017 г., 12:19
Я объяснил оба случая ... второй дает вам деление на ноль в показанных помещениях ... вы меня опровергли? Вот почему вы получаете деление на ноль ..... :) понизить меня ... но ответ был там ....
Решение Вопроса

gcc -O0 поведение объясняет разницу между вашими двумя версиями. (Покаclang -O0 случается, чтобы скомпилировать их обоих сidiv). И почему вы получаете это даже с операндами времени компиляции.x86idiv ошибочное поведение против поведения инструкции деления на ARM

Если целочисленная математика приводит к доставке сигнала, POSIX требует, чтобы он был SIGFPE:На каких платформах целочисленное деление на ноль вызывает исключение с плавающей запятой? Но POSIXне требуется перехват для любой конкретной целочисленной операции. (Именно поэтому разрешено отличаться для x86 и ARM).

Спецификация Single Unixопределяет SIGFPE как «Ошибочная арифметическая операция». Название с плавающей точкой вводит в заблуждение, но в нормальной системе с FPU в состоянии по умолчанию, только целочисленная математика вызовет его. На x86 только целочисленное деление. На MIPS компилятор может использоватьadd вместоaddu для подписанной математики, так что вы можете получить ловушки при переполнении подписанной надстройки. (GCC используетaddu даже для подписанного, но детектор с неопределенным поведением может использоватьadd.)

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

gcc без параметров такой же какgcc -O0.

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

Это объясняет разницу между вашими двумя версиями:

Не только делаетgcc -O0 не пытайтесь оптимизировать, это активноде-Оптимизирует сделать asm, который независимо реализует каждый оператор C внутри функции. Это позволяетgdb«sjump команда работать безопасно, позволяя вам перейти к другой строке внутри функции и вести себя так, будто вы действительно прыгаете в источнике Си.

Он также не может предполагать значения переменных между операторами, потому что вы можете изменять переменные с помощьюset b = 4, Это явно катастрофически плохо для производительности, поэтому-O0 код работает в несколько раз медленнее, чем обычный код, и почемуоптимизация для-O0 конкретно это полная ерунда, Это также делает-O0 вывод asmочень шумно и трудно для человека, чтобы читатьиз-за всего хранения / перезагрузки и отсутствия даже самых очевидных оптимизаций.

int a = 0x80000000;
int b = -1;
  // debugger can stop here on a breakpoint and modify b.
int c = a / b;        // a and b have to be treated as runtime variables, not constants.
printf("%d\n", c);

Я помещаю твой код в функциина Годболтепроводник компилятора чтобы получить asm для этих заявлений.

Оценитьa/b, gcc -O0 должен выдать код для перезагрузкиa а такжеb по памяти, и не делать никаких предположений об их значении.

Нос участиемint c = a / -1;Вы не можете изменить-1 с отладчиком, поэтому gcc может и действительно реализовать это утверждение так же, как оно будет реализовыватьint c = -a;с х86neg eax или AArch64neg w0, w0 инструкция, окруженная грузом (а) / магазином (с). На ARM32 этоrsb r3, r3, #0 (Обратное вычитание:r3 = 0 - r3).

Тем не менее, Clang5.0-O0 не делает эту оптимизацию. Он все еще используетidiv заa / -1так что обе версии выйдут из строя на x86 с clang. Почему gcc вообще "оптимизирует"? ВидетьОтключить все параметры оптимизации в GCC, gcc всегда преобразуется через внутреннее представление, а -O0 - это минимальный объем работы, необходимый для создания двоичного файла. У него нет «тупого и буквального» режима, который пытается сделать ассм как можно больше похожим на источник.

x86idiv против AArch64sdiv:

x86-64:

    # int c = a / b  from x86_fault()
    mov     eax, DWORD PTR [rbp-4]
    cdq                                 # dividend sign-extended into edx:eax
    idiv    DWORD PTR [rbp-8]           # divisor from memory
    mov     DWORD PTR [rbp-12], eax     # store quotient

в отличиеimul r32,r32нет 2-операндаidiv который не имеет дивидендов в верхней половине ввода. Во всяком случае, не это имеет значение; GCC использует его только сedx = копии знака бит вeaxтак что на самом деле он делает 32b / 32b => 32b частное + остаток.Как указано в руководстве Intel, idiv поднимает #DE на:

делитель = 0Подписанный результат (частное) слишком велик для получателя.

Переполнение может легко произойти, если вы используете полный диапазон делителей, например, заint result = long long / int с одним делением 64b / 32b => 32b. Но gcc не может выполнить эту оптимизацию, потому что не разрешено создавать код, который будет давать сбой, вместо того, чтобы следовать правилам целочисленного продвижения C и делать 64-битное деление итогда усекать доint, Это такжене оптимизирует даже в тех случаях, когда делитель, как известно, достаточно большой, чтобы он не мог#DE

При делении 32b / 32b (сcdq), единственный вход, который может переполнитьсяINT_MIN / -1, «Правильный» фактор - это 33-разрядное целое число со знаком, т.е.0x80000000 с битом начального нуля, чтобы сделать его положительным целым числом со знаком со знаком 2. Так как это не вписывается вeax, idiv поднимает#DE исключение. Ядро затем доставляетSIGFPE.

AArch64:

    # int c = a / b  from x86_fault()  (which doesn't fault on AArch64)
    ldr     w1, [sp, 12]
    ldr     w0, [sp, 8]          # 32-bit loads into 32-bit registers
    sdiv    w0, w1, w0           # 32 / 32 => 32 bit signed division
    str     w0, [sp, 4]

AFAICT, инструкции аппаратного деления ARM не вызывают исключений для деления на ноль или для INT_MIN / -1. Или по крайней мере,несколько Процессоры ARM этого не делают.делим на ноль исключение в процессоре ARM OMAP3515

AArch64sdiv документация не упоминает никаких исключений.

Однако программные реализации целочисленного деления могут вызывать:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka4061.html, (gcc использует библиотечный вызов для деления на ARM32 по умолчанию, если вы не установили -mcpu с делением HW.)

C Неопределенное поведение.

КакPSkocik объясняет, INT_MIN / -1 является неопределенным поведением в C, как и все целочисленные переполнения со знаком.Это позволяет компиляторам использовать инструкции аппаратного деления на машинах, таких как x86, без проверки этого особого случая. Если бы это былоне ошибка, неизвестные входные данные потребуют проверки во время выполнения и проверки ветвления, и никто не хочет, чтобы C требовал этого.

Подробнее о последствиях UB:

С включенной оптимизациейкомпилятор может предположить, чтоa а такжеb все еще имеют свои установленные значения, когдаa/b пробеги. Затем он может видеть, что программа имеет неопределенное поведение, и, следовательно, может делать все, что захочет. GCC выбирает производитьINT_MIN как это было бы от-INT_MIN.

В системе дополнения 2 наиболее отрицательное число является его собственным отрицательным числом. Это неприятный угловой футляр для дополнения 2, потому что это означаетabs(x) все еще может быть отрицательным.https://en.wikipedia.org/wiki/Two%27s_complement#Most_negative_number

int x86_fault() {
    int a = 0x80000000;
    int b = -1;
    int c = a / b;
    return c;
}

скомпилировать это сgcc6.3 -O3 для x86-64

x86_fault:
    mov     eax, -2147483648
    ret

ноclang5.0 -O3 компилируется в (без предупреждения даже с -Wall -Wextra`):

x86_fault:
    ret

Неопределенное поведение на самом деле совершенно не определено. Компиляторы могут делать все что угодно, в том числе возвращать мусор, который был вeax при вводе функции или загрузке нулевого указателя и недопустимой инструкции. например с gcc6.3 -O3 для x86-64:

int *local_address(int a) {
    return &a;
}

local_address:
    xor     eax, eax     # return 0
    ret

void foo() {
    int *p = local_address(4);
    *p = 2;
}

 foo:
   mov     DWORD PTR ds:0, 0     # store immediate 0 into absolute address 0
   ud2                           # illegal instruction

Ваш случай с-O0 не позволил компиляторам видеть UB во время компиляции, поэтому вы получили «ожидаемый» вывод asm.

Смотрите такжеЧто каждый программист C должен знать о неопределенном поведении (тот же пост в блоге LLVM, на который ссылается Базиль).

 Peter Cordes23 сент. 2017 г., 19:39
Почему это было отвергнуто? Я сделал это слишком долго и бессвязно? Я не хотел предполагать очень много начальных знаний, но это сделало его длинным ответом: /
 iBug24 сент. 2017 г., 03:09
Я проверил вывод сборки и сразу понял, что происходит, когда увиделa/-1; был составлен вnegl %eax покаa/b был скомпилирован вcltd; idivl %ebx, Спасибо за ваше длинное объяснение и терпение!

Ваш ответ на вопрос