Обнаружение переполнения со знаком в C / C++

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

я обнаружил, что при обнаружении переполнения целого числа без знака довольно тривиально, обнаруживая подпись переполнения в c/" class="blnk">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, переполнение целого числа со знаком неопределенное поведение. другими словами, согласно стандарту, как только вы даже вызываете переполнение со знаком, ваша программа так же недействительна, как если бы вы разыграли нулевой указатель. Таким образом, вы не можете вызвать неопределенное поведение, а затем попытаться обнаружить переполнение после факта, как в приведенном выше примере проверки post-condition.

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

Итак, еще один возможный способ проверить переполнение:

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 похоже на выражение, которое компилятор может оптимизировать, думая, что подписанное переполнение невозможно.

Так есть ли лучшее решение здесь? Что-то, что гарантированно 1) не вызывает неопределенного поведения, и 2) не предоставляет компилятору возможность оптимизировать проверки переполнения? Я думал, что может быть какой-то способ сделать это, приведя оба операнда к unsigned и выполнив проверки, свернув свою собственную арифметику дополнения, но я не совсем уверен, как это сделать.

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-sized добавление и одиночное условное скачк-на-переполнение и никогда не совершать больше того.

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 С обеих сторон неравенств, и это дает вам точно арифметические условия, что ваш результат выходит за рамки.


ИМХО, eastiest способ борьбы с переполнением sentsitive C++ код, чтобы использовать SafeInt<T>. Это кросс-платформенный шаблон c++, размещенный на коде plex, который обеспечивает гарантии безопасности, которые вы хотите здесь.

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


для случая gcc, от GCC 5.0 примечания к выпуску теперь мы видим, что он предоставляет __builtin_add_overflow для проверки переполнения дополнительно:

добавлен новый набор встроенных функций для арифметики с проверкой переполнения: _ _ builtin_add _ overflow, _ _ builtin _ sub _ overflow и _ _ builtin_mul_overflow и для совместимости с clang также другие варианты. Эти встроенные элементы имеют два интегральных аргумента (которые не должны иметь один и тот же тип), аргументы расширены для бесконечной точности на них выполняется знаковый тип+, - или*, а результат сохраняется в целочисленной переменной, на которую указывает последний аргумент. Если сохраненное значение равно результату бесконечной точности, встроенные функции возвращают false, в противном случае true. Тип целочисленной переменной, которая будет содержать результат, может отличаться от типов первых двух аргументов.

например:

__builtin_add_overflow( rhs, lhs, &result )

мы можем видеть из документа gcc встроенные функции для выполнения арифметики с проверкой переполнения что:

[...]эти встроенные функции имеют полностью определенное поведение для всех значений аргументов.

clang также предоставляет набор проверено арифметическое builtins:

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

в этом случае строение будет:

__builtin_sadd_overflow( rhs, lhs, &result )

Если вы используете встроенный ассемблер, вы можете проверить флаг переполнения. Другая возможность-taht вы можете использовать тип safeint. Я рекомендую прочитать эту статью на Integer Security.


как насчет:

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 builtin:

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 builtins, следующий самый быстрый способ-использовать битовые операции над битами знака. Знаковое переполнение дополнительно возникает, когда:

  • два операнда имеют один и тот же знак, и
  • результат имеет другой знак, чем операнды.

знак бит ~(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 

приводит к неопределенному поведению? Нет такой вещи в refference набора инструкций Intel x86..


очевидным решением является преобразование в 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 = (без volatile или register) функция не делала тест и делала добавление, используя только один lea инструкции (lea является эффективным адресом загрузки и часто используется для добавления, не касаясь регистра флагов).

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