Обнаружение переполнения со знаком в C / C++
на первый взгляд, этот вопрос может показаться дубликат как обнаружить переполнение целого числа?, однако это на самом деле существенно отличаются.
я обнаружил, что при обнаружении переполнения целого числа без знака довольно тривиально, обнаруживая подпись переполнения в C/C++ на самом деле сложнее, чем думает большинство людей.
наиболее очевидный, но наивный способ сделать это было бы что-то вроде:
int add(int lhs, int rhs)
{
int sum = lhs + rhs;
if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) {
/* an overflow has occurred */
abort();
}
return sum;
}
проблема с это означает, что в соответствии со стандартом C переполнение целого числа со знаком равно неопределенное поведение.
хотя вышеуказанная проверка, вероятно, будет работать на многих компиляторах, вы не можете рассчитывать на это. Фактически, поскольку стандарт C говорит, что переполнение целого числа со знаком не определено, некоторые компиляторы (например, GCC) будут оптимизация прочь выше проверки когда установлены флаги оптимизации, потому что компилятор предполагает, что переполнение со знаком невозможно. Это полностью нарушает попытку проверить переполнение. таким образом, еще один возможный способ проверить переполнение будет: это кажется более перспективным, так как мы на самом деле не добавляем два целых числа вместе, пока мы не убедимся заранее, что выполнение такого добавления не приведет к переполнению. Таким образом, мы не вызываем неопределенного поведения. однако это решение, к сожалению, намного менее эффективно, чем исходное решение, так как вам нужно выполнить операцию вычитания, чтобы проверить, будет ли работать ваша операция сложения. И даже если вы не заботитесь об этом (небольшом) хите производительности, я все еще не полностью убежден, что это решение адекватно. Выражение Так есть ли лучшее решение здесь? Что-то, что гарантированно 1) не вызывает неопределенное поведение, и 2) не дают компилятору возможность оптимизировать проверки переполнения? Я думал, что может быть какой-то способ сделать это, бросив оба операнда в unsigned и выполнив проверки, свернув свою собственную арифметику с двумя дополнениями, но я не совсем уверен, как это сделать.int add(int lhs, int rhs)
{
if (lhs >= 0 && rhs >= 0) {
if (INT_MAX - lhs <= rhs) {
/* overflow has occurred */
abort();
}
}
else if (lhs < 0 && rhs < 0) {
if (lhs <= INT_MIN - rhs) {
/* overflow has occurred */
abort();
}
}
return lhs + rhs;
}
lhs <= INT_MIN - rhs
похоже на то выражение, которое компилятор может оптимизировать, думая, что переполнение со знаком невозможно.
12 ответов:
ваш подход с вычитанием является правильным и четким. Компилятор не может оптимизировать его.
другой правильный подход, если у вас есть более крупный целочисленный тип, состоит в том, чтобы выполнить арифметику в большем типе, а затем проверить, что результат соответствует меньшему типу при преобразовании его обратно
int sum(int a, int b) { long long c; assert(LLONG_MAX>INT_MAX); c = (long long)a + b; if (c < INT_MIN || c > INT_MAX) abort(); return c; }
хороший компилятор должен преобразовать все дополнение и
if
заявление вint
-размер сложения и один условный переход на переполнение и никогда не совершать больше того.Edit: как отметил Стивен, у меня возникли проблемы с получением (не очень хорошего) компилятора gcc для создания вменяемого asm. Код, который он генерирует, не очень медленный, но, безусловно, неоптимальный. Если кто-нибудь знает варианты этого кода, которые заставят gcc делать правильные вещи, я бы хотел их увидеть.
нет, ваш 2-й код не правильный, но вы близки: если вы установите
int half = INT_MAX/2; int half1 = half + 1;
результат сложения
INT_MAX
. (INT_MAX
- всегда нечетное число). Так что это допустимый вход. Но в вашей рутине вы будете иметьINT_MAX - half == half1
и вы бы прервать. Ложное срабатывание.эту ошибку можно исправить, поставив
<
вместо<=
в обоих чеков.но тогда и ваш код не является оптимальным. Следующее будет делать:
int add(int lhs, int rhs) { if (lhs >= 0) { if (INT_MAX - lhs < rhs) { /* would overflow */ abort(); } } else { if (rhs < INT_MIN - lhs) { /* would overflow */ abort(); } } return lhs + rhs; }
посмотреть что это действительно, Вы должны символически добавить
lhs
по обе стороны неравенств, и это дает вам точно арифметические условия, что ваш результат выходит за пределы.
IMHO, самый восточный способ справиться с переполнением sentsitive C++ - это использовать
SafeInt<T>
. Это кросс-платформенный шаблон C++, размещенный на Code plex, который обеспечивает гарантии безопасности, которые вы хотите здесь.Я нахожу его очень интуитивным в использовании, поскольку он обеспечивает многие из тех же шаблонов использования, что и обычные числовые операции, и выражает над и под потоками через исключения.
для случая gcc, от примечания к выпуску gcc 5.0 мы видим, что теперь он обеспечивает
__builtin_add_overflow
для проверки переполнения дополнительно:новый набор встроенных функций для арифметики с переполнением проверки добавлен: __строение_добавить_переполнения, __строение_суб_переполнения и __строение_ООО_переполнения, а также для совместимости с лязгом и другие варианты. Эти встроенные элементы имеют два интегральных аргумента (которые не должны иметь один и тот же тип), аргументы расширяются для бесконечной точности знаковый тип,+, - или * выполняется на тех, и результат хранится в целочисленной переменной, на которую указывает последний аргумент. Если сохраненное значение равно результату бесконечной точности, встроенные функции возвращают false, в противном случае true. Тип целочисленной переменной, которая будет содержать результат, может отличаться от типов первых двух аргументов.
например:
__builtin_add_overflow( rhs, lhs, &result )
мы можем видеть из документа gcc встроенные функции для выполнения арифметики с проверкой переполнения что:
[...]эти встроенные функции имеют полностью определенное поведение для всех значений аргументов.
clang также предоставляет набор проверенные арифметические встроенные:
Clang предоставляет набор встроенных функций, которые реализуют проверенную арифметику для критически важных приложений безопасности таким образом, что это быстро и легко выражается в С.
в этом случае строение будет:
__builtin_sadd_overflow( rhs, lhs, &result )
Если вы используете встроенный ассемблер, вы можете проверить флаг переполнения. Другая возможность заключается в том, что вы можете использовать тип safeint. Я рекомендую прочитать эту статью на Целое Число Безопасности.
как насчет:
int sum(int n1, int n2) { int result; if (n1 >= 0) { result = (n1 - INT_MAX)+n2; /* Can't overflow */ if (result > 0) return INT_MAX; else return (result + INT_MAX); } else { result = (n1 - INT_MIN)+n2; /* Can't overflow */ if (0 > result) return INT_MIN; else return (result + INT_MIN); } }
Я думаю, что это должно работать для любой законной
INT_MIN
иINT_MAX
(симметрично или нет); функция, как показано клипы, но это должно быть очевидно, как получить другие поведения).
возможно, Вам повезет с преобразованием в 64-разрядные целые числа и тестированием подобных условий. Например:
#include <stdint.h> ... int64_t sum = (int64_t)lhs + (int64_t)rhs; if (sum < INT_MIN || sum > INT_MAX) { // Overflow occurred! } else { return sum; }
вы можете поближе взглянуть на то, как расширение знака будет работать здесь, но я думаю, что это правильно.
самый быстрый способ-использовать встроенный GCC:
int add(int lhs, int rhs) { int sum; if (__builtin_add_overflow(lhs, rhs, &sum)) abort(); return sum; }
на x86, GCC компилирует это:
mov %edi, %eax add %esi, %eax jo call_abort ret call_abort: call abort
который использует встроенное обнаружение переполнения процессора.
если вы не в порядке с использованием встроенных GCC, следующий быстрый способ-использовать битовые операции над знаковыми битами. Кроме того, переполнение со знаком происходит, когда:
- два операнда имеют одинаковый знак, и
- результат имеет другой знак, чем операнды.
знак бит
~(lhs ^ rhs)
находится на iff операнды имеют тот же знак, и знак битlhs ^ sum
находится на iff результат имеет другой знак, чем операнды. Таким образом, вы можете сделать добавление в неподписанной форме, чтобы избежать неопределенного поведения, а затем использовать знаковый бит~(lhs ^ rhs) & (lhs ^ sum)
:int add(int lhs, int rhs) { unsigned sum = (unsigned) lhs + (unsigned) rhs; if ((~(lhs ^ rhs) & (lhs ^ sum)) & 0x80000000) abort(); return (int) sum; }
это компилируется в:
lea (%rsi,%rdi), %eax xor %edi, %esi not %esi xor %eax, %edi test %edi, %esi js call_abort ret call_abort: call abort
что намного быстрее, чем приведение к 64-разрядному типу на 32-разрядной машине (с gcc):
push %ebx mov 12(%esp), %ecx mov 8(%esp), %eax mov %ecx, %ebx sar , %ebx clt add %ecx, %eax adc %ebx, %edx mov %eax, %ecx add $-2147483648, %ecx mov %edx, %ebx adc , %ebx cmp , %ebx ja call_abort pop %ebx ret call_abort: call abort
по мне, самая простая проверка будет проверка знаков операндов и результатов.
рассмотрим sum: переполнение может происходить в обоих направлениях, + или -, только когда оба операнда имеют одинаковый знак. И, очевидно, переполнение будет, когда знак результата не будет таким же, как знак операндов.
Итак, такой чек будет достаточно:
int a, b, sum; sum = a + b; if (((a ^ ~b) & (a ^ sum)) & 0x80000000) detect_oveflow();
Edit: как предложил Нильс, это правильно
if
состояние:((((unsigned int)a ^ ~(unsigned int)b) & ((unsigned int)a ^ (unsigned int)sum)) & 0x80000000)
и с каких пор инструкция
add eax, ebx
приводит к неопределенному поведению? Нет такой вещи в наборе инструкций Intel x86 refference..
очевидным решением является преобразование в unsigned, чтобы получить четко определенное поведение переполнения без знака:
int add(int lhs, int rhs) { int sum = (unsigned)lhs + (unsigned)rhs; if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) { /* an overflow has occurred */ abort(); } return sum; }
это заменяет неопределенное поведение переполнения со знаком с определенным реализацией преобразованием значений вне диапазона между подписанными и беззнаковыми, поэтому вам нужно проверить документацию вашего компилятора, чтобы точно знать, что произойдет, но она должна быть по крайней мере хорошо определена и должна делать правильные вещи на любой машине с двумя дополнениями, которая не вызывает сигналов о преобразованиях, которые в значительной степени являются каждой машиной и компилятором C, построенным за последние 20 лет.
в случае добавления двух
long
значения, портативный код может разделитьlong
значение в низкий и высокийint
запасные части (или вshort
запасные части в случаеlong
имеет тот же размер, что иint
):static_assert(sizeof(long) == 2*sizeof(int), ""); long a, b; int ai[2] = {int(a), int(a >> (8*sizeof(int)))}; int bi[2] = {int(b), int(b >> (8*sizeof(int))}); ... use the 'long' type to add the elements of 'ai' and 'bi'
использование встроенной сборки-это самый быстрый способ, если вы нацелены на конкретный процессор:
long a, b; bool overflow; #ifdef __amd64__ asm ( "addq %2, %0; seto %1" : "+r" (a), "=ro" (overflow) : "ro" (b) ); #else #error "unsupported CPU" #endif if(overflow) ... // The result is stored in variable 'a'
Я думаю, что это работает:
int add(int lhs, int rhs) { volatile int sum = lhs + rhs; if (lhs != (sum - rhs) ) { /* overflow */ //errno = ERANGE; abort(); } return sum; }
использование volatile не позволяет компилятору оптимизировать тест, потому что он думает, что
sum
может измениться между сложением и вычитанием.использование gcc 4.4.3 для x86_64 сборка для этого кода выполняет сложение, вычитание и тест, хотя она хранит все в стеке и ненужных операциях стека. Я даже пытался
register volatile int sum =
но сборка была такой же.для a версия только с
int sum =
(нет летучих или регистр) функция не делала тест и сделал добавление, используя только одинlea
инструкции (lea
является эффективным адресом загрузки и часто используется для добавления, не касаясь регистра флагов).ваша версия больше кода и имеет гораздо больше прыжков, но я не знаю, что будет лучше.