Обнаружение переполнения со знаком в C / C ++ - PullRequest
74 голосов
/ 15 октября 2010

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

Я обнаружил, что при обнаружении неподписанногоЦелочисленное переполнение довольно тривиально, обнаружить переполнение в виде подписи в 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) будут оптимизировать вышеприведенную проверку , когда установлены флаги оптимизации, поскольку компилятор предполагает, что переполнение со знаком невозможно.Это полностью исключает попытку проверки на переполнение.

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

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) не предоставляет компилятору возможность оптимизировать проверку переполнения?Я думал, что мог бы быть какой-то способ сделать это, приведя оба операнда к беззнаковому, и выполнив проверки, свернув арифметику с двумя собственными дополнениями, но я не совсем уверен, как это сделать.

Ответы [ 12 ]

34 голосов
/ 16 октября 2010

Нет, ваш второй код неверен, но вы близки: если вы установите

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

23 голосов
/ 15 октября 2010

Ваш подход с вычитанием правильный и четкий.Компилятор не может оптимизировать его.

Другой правильный подход, если у вас есть доступный целочисленный тип большего размера, состоит в том, чтобы выполнить арифметику в большем типе и затем проверить, соответствует ли результат меньшему типу при преобразовании его обратно.

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

Редактировать: Как указал Стивен, у меня проблемы с получением (не очень хорошего) компилятора gcc для генерации вменяемого ассма.Код, который он генерирует, не очень медленный, но, безусловно, неоптимальный.Если кто-нибудь знает варианты в этом коде, которые заставят gcc сделать правильные вещи, я хотел бы увидеть их.

16 голосов
/ 15 октября 2010

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

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

13 голосов
/ 31 августа 2015

Для случая 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 также предоставляет набор проверенных арифметических встроенных функций :

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

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

__builtin_sadd_overflow( rhs, lhs, &result )
10 голосов
/ 15 октября 2010

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

5 голосов
/ 29 июня 2017

Самый быстрый способ - использовать встроенную в 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) установлен, если операнды имеют тот же знак, а бит знака lhs ^ sum включен, если результат имеет знак, отличный от операндов. Таким образом, вы можете выполнить сложение в виде без знака, чтобы избежать неопределенного поведения, а затем использовать знаковый бит ~(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 $31, %ebx
    clt
    add %ecx, %eax
    adc %ebx, %edx
    mov %eax, %ecx
    add $-2147483648, %ecx
    mov %edx, %ebx
    adc $0, %ebx
    cmp $0, %ebx
    ja call_abort
    pop %ebx
    ret
call_abort:
    call abort
2 голосов
/ 16 октября 2010

Как насчет:

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

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

Вам может повезти, если вы преобразуете в 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;
}

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

0 голосов
/ 06 июня 2017

В случае добавления двух значений 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'
0 голосов
/ 16 октября 2010

Очевидное решение - преобразовать в 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 лет.

...