检测C / C ++中的带符号溢出

时间:2010-10-15 17:16:11

标签: c++ c undefined-behavior signed integer-overflow

乍一看,这个问题可能看起来像是How to detect integer overflow?的副本,但实际上却有很大不同。

我发现虽然检测无符号整数溢出非常简单,但在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)将optimize away the above check,因为编译器假定有符号溢出是不可能的。这完全打破了检查溢出的尝试。

因此,检查溢出的另一种可能方法是:

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 个答案:

答案 0 :(得分:34)

不,你的第二个代码不正确,但你很接近:如果你设置

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,这样就可以准确地给出结果超出界限的算术条件。

答案 1 :(得分:23)

您的减法方法是正确且明确的。编译器无法对其进行优化。

如果你有一个更大的整数类型,另一个正确的方法是在更大的类型中执行算术,然后在转换它时检查结果是否适合较小的类型

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来生成理智的asm。它生成的代码并不是非常慢,但肯定不是最理想的。如果有人知道这个代码的变体会让gcc做正确的事情,我很乐意看到它们。

答案 2 :(得分:16)

恕我直言,处理溢出的sentsitive C ++代码的最东方方法是使用SafeInt<T>。这是一个托管在代码plex上的跨平台C ++模板,它提供了您所需的安全保证。

我发现使用它非常直观,因为它提供了许多与正常数值操作相同的使用模式,并通过异常表示流量和流量。

答案 3 :(得分:13)

对于gcc案例,从gcc 5.0 Release notes我们可以看到它现在提供__builtin_add_overflow来检查溢出:

  

添加了一组用于具有溢出检查的算术的内置函数:__ builtin_add_overflow,__ builtin_sub_overflow和__builtin_mul_overflow以及与clang兼容的其他变体。这些内置函数有两个整数参数(不需要具有相同的类型),参数扩展为无限精度签名类型,+, - 或*对它们执行,结果存储在整数变量中最后一个论点指出。如果存储的值等于无限精度结果,则内置函数返回false,否则返回true。保存结果的整数变量的类型可以与前两个参数的类型不同。

例如:

__builtin_add_overflow( rhs, lhs, &result )

我们可以从gcc文档Built-in Functions to Perform Arithmetic with Overflow Checking中看到:

  

[...]这些内置函数对所有参数值都有完全定义的行为。

clang还提供了一组checked arithmetic builtins

  

Clang提供了一组内置函数,它们以一种在C中快速且易于表达的方式为安全关键应用程序实现检查算法。

在这种情况下,内置将是:

__builtin_sadd_overflow( rhs, lhs, &result )

答案 4 :(得分:10)

如果使用内联汇编程序,则可以查看overflow flag。另一种可能性是你可以使用safeint datatype。我建议您在Integer Security上阅读本文。

答案 5 :(得分:5)

最快的方法是使用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)的符号位为on,如果结果与操作数的符号不同,则lhs ^ sum的符号位为on。因此,您可以使用无符号形式进行添加以避免未定义的行为,然后使用~(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

比在32位机器上使用64位类型(使用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

答案 6 :(得分:2)

怎么样:

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_MININT_MAX(对称与否);如图所示的剪辑功能,但应该明白如何获得其他行为)。

答案 7 :(得分:1)

你可能会更好地转换为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;
}

您可能需要仔细查看符号扩展如何在此处运行,但我认为这是正确的。

答案 8 :(得分:0)

通过我,最简单的检查将是检查操作数和结果的迹象。

让我们检查一下:只有当两个操作数具有相同的符号时,才会在两个方向上发生溢出,+或 - 。而且,显而易见的是,溢出将是当结果的符号与操作数的符号不同时。

所以,这样的检查就足够了:

int a, b, sum;
sum = a + b;
if  (((a ^ ~b) & (a ^ sum)) & 0x80000000)
    detect_oveflow();

编辑:正如Nils建议的那样,这是正确的if条件:

((((unsigned int)a ^ ~(unsigned int)b) & ((unsigned int)a ^ (unsigned int)sum)) & 0x80000000)

从那时开始说明

add eax, ebx 

导致未定义的行为?英特尔x86指令集参考中没有这样的东西..

答案 9 :(得分:0)

显而易见的解决方案是转换为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;  
} 

这将使用有符号和无符号之间的超出范围值的实现定义转换替换未定义的有符号溢出行为,因此您需要检查编译器的文档以确切知道将发生什么,但它至少应该是好的定义,并且应该在任何不会在转换上产生信号的二进制补码机器上做正确的事情,这几乎是过去20年内构建的每台机器和C编译器。

答案 10 :(得分:0)

如果添加两个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'

如果定位特定的CPU,使用内联汇编是最快的方法:

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'

答案 11 :(得分:-1)

我认为这有效:

int add(int lhs, int rhs) {
   volatile int sum = lhs + rhs;
   if (lhs != (sum - rhs) ) {
       /* overflow */
       //errno = ERANGE;
       abort();
   }
   return sum;
}

使用volatile可以防止编译器优化测试,因为它认为sum可能在加法和减法之间发生了变化。

对于x86_64使用gcc 4.4.3,此代码的程序集会执行加法,减法和测试,但它会将所有内容存储在堆栈中以及不需要的堆栈操作。我甚至试过register volatile int sum =,但装配是一样的。

对于只有int sum =(没有易失性或寄存器)的版本,该函数没有进行测试,只使用一条lea指令进行添加(lea是加载有效地址和通常用于在不触及标志寄存器的情况下进行添加。)

你的版本是更大的代码并且有更多的跳转,但我不知道哪个更好