Переполнение со знаком является неопределенным поведением в ISO C. Вы не можете надежно вызвать его и затем проверить, произошло ли это.
В выражении (x + y) > y;
компиляторДопускается предположить, что x+y
не переполняется (потому что это будет UB).Поэтому он оптимизируется вплоть до проверки x > 0
. (Да, действительно, gcc делает это даже при -O0
).
Эта оптимизация является новой в gcc8.То же самое на x86 и AArch64;вы должны были использовать разные версии GCC на AArch64 и x86 .(Даже в -O3
, gcc7.x и более ранних (намеренно?) Пропускают эту оптимизацию. Clang7.0 тоже не делает этого. Они на самом деле делают 32-битные операции добавления и сравнения. Они также пропускают оптимизацию tadd_ok
для return 1
или add
и проверка флага переполнения (V
в ARM, OF
в x86). Оптимизированный asm Clang представляет собой интересное сочетание >>31
, OR и одной операции XOR, но на самом деле -fwrapv
изменяет этот asm, так что, вероятно, он не выполняет полную проверку переполнения.)
Можно сказать, что gcc8 «ломает» ваш код, но на самом деле он уже был сломан, поскольку он был легальным / переносимым ISO C. gcc8 только что показалтот факт.
Чтобы увидеть это более четко, давайте выделим только это выражение в одну функцию.gcc -O0
в любом случае компилирует каждый оператор отдельно, поэтому информация, которая запускается только тогда, когда x<0
не влияет на код кода -O0
для этого оператора в вашей функции tadd_ok
.
// compiles to add and checking the carry flag, or equivalent
int unsigned_overflow_test(unsigned x, unsigned y) {
return (x+y) >= y; // unsigned overflow is well-defined as wrapping.
}
// doesn't work because of UB.
int signed_overflow_expression(int x, int y) {
return (x+y) > y;
}
В проводнике компилятора Godbolt с AArch64 GCC8.2 -O0 -fverbose-asm
:
signed_overflow_expression:
sub sp, sp, #16 //,, // make a stack fram
str w0, [sp, 12] // x, x // spill the args
str w1, [sp, 8] // y, y
// end of prologue
// instructions that implement return (x+y) > y; as return x > 0
ldr w0, [sp, 12] // tmp94, x
cmp w0, 0 // tmp94,
cset w0, gt // tmp95, // w0 = (x>0) ? 1 : 0
and w0, w0, 255 // _1, tmp93 // redundant
// epilogue
add sp, sp, 16 //,,
ret
GCC -ftree-dump-original
или -optimized
даже превратит свой GIMPLE обратно вC-подобный код с этой оптимизацией (по ссылке Godbolt):
;; Function signed_overflow_expression (null)
;; enabled by -tree-original
{
return x > 0;
}
К сожалению, даже с -Wall -Wextra -Wpedantic
нет никаких предупреждений о сравнении.Это не тривиально правда;это все еще зависит от x
.
Оптимизированный asm неудивительно cmp w0, 0
/ cset w0, gt
/ ret
.AND с 0xff
является избыточным.cset
- псевдоним csinc
, использующий нулевой регистр в качестве обоих источников.Таким образом, он выдаст 0 / 1. Для других регистров общий случай csinc
- это условный выбор и приращение любых 2 регистров.
В любом случае, cset
- это эквивалент AArch64 для x86 setcc
,для преобразования состояния флага в регистр bool
.
Если вы хотите, чтобы ваш код работал так, как написано, вам нужно скомпилировать с -fwrapv
, чтобы сделать это хорошо определенным поведением в варианте C, который -fwrapv
заставляет GCC реализовать.По умолчанию -fstrict-overflow
, как и в стандарте ISO C.
Если вы хотите проверить переполнение со знаком в современном C, вам нужно написать проверки, которые обнаруживают переполнение , фактически не вызывая его. Это сложнее, раздражает и вызывает споры между авторами компиляторов и (некоторыми) разработчиками.Они утверждают, что языковые правила вокруг неопределенного поведения не предназначались для использования в качестве предлога для «бесполезного разрыва» кода при компиляции для целевых машин, где это имело бы смысл в asm.Но современные компиляторы в основном реализуют только ISO C (с некоторыми расширениями и дополнительным определенным поведением), даже при компиляции для целевых архитектур, таких как x86 и ARM, где целые числа со знаком не имеют дополнения (и, таким образом, просто переносятся) и не перехватывают переполнение.
Таким образом, вы могли бы сказать «выстрелы» в этой войне, с изменением в gcc8.x на фактическое «взлом» небезопасного кода, подобного этому.: P
См. Обнаружение переполнения со знаком в C / C ++ и Как проверить переполнение со знаком в C без неопределенного поведения?
Поскольку добавление со знаком и без знака является одной и той же двоичной операцией в дополнении к 2 , вы можете , возможно, просто привести к unsigned
для добавления и отвести обратно дляподписанное сравнение.Это сделало бы версию вашей функции безопасной для «нормальных» реализаций: дополнение 2, а приведение между unsigned
и int
- это просто переосмысление тех же битов.
У этого не может быть UB, просто он не даст правильного ответа в своих дополнениях или реализациях знака / величины C.
return (int)((unsigned)x + (unsigned)y) > y;
Компилируется (с gcc8.2 -O3 для AArch64)
add w0, w0, w1 // x+y
cmp w0, w1 // x+y cmp y
cset w0, gt
ret
Если бы вы написали int sum = x+y
как отдельный оператор C из return sum < y
, этот UB не был бы виден для gcc с отключенной оптимизацией. Нокак часть того же выражения, даже gcc
со значением по умолчанию -O0
может видеть его.
UB, видимый во время компиляции, - это все виды ошибок.В этом случае только определенные диапазоны входов будут генерировать UB, поэтому компилятор предполагает, что этого не происходит.Если безусловный UB виден на пути выполнения, оптимизирующий компилятор может предположить, что путь никогда не произойдет.(В функции без ветвления можно предположить, что эта функция никогда не вызывается, и скомпилировать ее в одну недопустимую инструкцию.) См. Допускает ли стандарт C ++ неинициализированный bool для сбоя программы? для получения дополнительной информации.о UB, видимом во время компиляции.
(-O0
не означает «нет оптимизации», это означает, что нет extra оптимизации, кроме того, что уже необходимо для преобразования через внутренние представления gcc вспособ ассемблировать для любой целевой платформы. @Basile Starynkevitch объясняет в Отключить все опции оптимизации в GCC )
Некоторые другие компиляторы могут еще больше «отключить свой мозг» при отключенной оптимизации, и сделатьчто-то ближе к транслитерации C в asm, но gcc не , как это.Например, gcc по-прежнему использует мультипликативный обратный для целочисленного деления на константу в -O0
.( Почему GCC использует умножение на странное число при реализации целочисленного деления? ) Все 3 других основных компилятора x86 (clang / ICC / MSVC) используют div
.