在C / C ++中检测签名溢出


81

乍一看,这个问题似乎与“如何检测整数溢出”重复出现,但实际上有很大的不同。

我发现,虽然检测无符号整数溢出非常简单,但是在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)不能为编译器提供优化溢出检查的机会?我当时想通过将两个操作数都转换为无符号,然后通过滚动自己的二进制补码算术执行检查可能有某种方法,但是我不确定如何做到这一点。


1
而是尝试检测,编写没有溢出可能性的代码不是更好的追求吗?
阿伦(Arun)2010年

9
@ArunSaha:很难进行计算以确保它们不会溢出,并且在一般情况下无法证明。通常的做法是使用尽可能宽的整数类型并希望如此。
David Thornley 2010年

6
@Amardeep:取消引用空指针同样没有定义为有符号溢出。未定义的行为意味着,就标准而言,任何事情都可能发生。不能认为签名溢出后系统不会处于无效和不稳定的状态。OP指出了这样一个后果:优化程序删除一旦发现有符号溢出的代码是完全合法的。
David Thornley 2010年

16
@Amardeep:我提到了这样的实现。设置优化标志后,GCC会删除溢出检查代码。因此,它基本上会破坏您的程序。可以说这比空指针取消引用更糟,因为它可能导致细微的安全漏洞,而取消引用空引用可能只会用段错误直接破坏程序。
2010年

2
@Amardeep:我当然似乎在实现中,根据编译器设置的不同,溢出会导致陷阱。如果语言允许人们指定特定的无符号变量或数量应该(1)干净地包装,(2)错误或(3)做任何方便的事情,那将是很好的。请注意,如果变量小于机器的寄存器大小,则要求无符号数要整齐包装可能会阻止生成最佳代码。
超级猫

Answers:


26

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

如果您有较大的整数类型可用,另一种正确的方法是在较大的类型上执行算术,然后在将其转换回时检查结果是否适合较小的类型

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-size的加法和一个有条件的溢出跳转,并且切勿实际执行较大的加法。

编辑:正如斯蒂芬指出的那样,我在获取一个(不太好的)编译器gcc来生成合理的asm时遇到了麻烦。它生成的代码并不是很慢,但是肯定不是最优的。如果有人知道此代码的变体,这些变体将使gcc做正确的事情,我很乐意看到它们。


1
对于任何想使用它的人,请确保您正在查看我的编辑版本。在原始版本中,我愚蠢地省略了添加long long之前的强制转换。
R .. GitHub停止帮助ICE,2010年

2
出于好奇,您是否成功让编译器进行了此优化?对一些编译器进行的快速测试并没有发现任何可能的结果。
斯蒂芬·佳能

2
在x86_64上,使用32位整数没有任何效率低下。性能与64位相同。使用小于本机字长类型的一种动机是,由于溢出/进位发生在直接可访问的位置,因此处理溢出条件或进行进位(对于任意精度算术)极其有效。
R .. GitHub停止帮助ICE,2010年

2
@R。,@ Steven:OP提供的减法代码不正确,请参见我的回答。我在那里也提供了一个代码,最多可以进行两次比较。也许编译器会做得更好。
詹斯·古斯特

3
这种方法不适用于的罕见平台 sizeof(long long) == sizeof(int)。C仅指定sizeof(long long) >= sizeof(int)
chux-恢复莫妮卡2014年

36

不,您的第二个密码不正确,但是您很接近:如果您设置

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为最佳答案。次要的:建议/* overflow will occurred */强调一点是,如果代码没有lhs + rhs实际求和就检测出溢出将会发生。
chux-恢复莫妮卡2014年

16

恕我直言,处理溢出的敏感C ++代码最简便的方法是使用SafeInt<T>。这是托管在代码plex上的跨平台C ++模板,可在此处提供您所需的安全性保证。

我发现它使用起来非常直观,因为它提供了许多与普通数值运算相同的使用模式,并通过异常表示了上下流动。


14

对于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 )

该功能似乎非常有用,除了以下几点:int result; __builtin_add_overflow(INT_MAX, 1, &result);没有明确说明result在溢出时存储的内容,不幸的是,在指定发生未定义行为的情况下它是安静的。当然,这是目的-没有UB。如果指定的话更好。
chux-恢复莫妮卡2015年

1
@chux好点,它指出这里的结果始终是定义的,我更新了答案。如果不是这种情况,那将非常具有讽刺意味。
沙菲克·雅格慕

有趣的是您的新参考没有(unsigned) long long *resultfor __builtin_(s/u)addll_overflow。当然这些是错误的。令人怀疑其他方面的准确性。IAC,很高兴看到这些__builtin_add/sub/mull_overflow()。希望他们有一天能达到C规范。
chux-恢复莫妮卡2015年

1
+1生成汇编程序比标准C语言中的任何汇编程序都要好得多,至少在没有依靠编译器的优化器来确定正在执行的操作的情况下如此。应该检测出何时有此类内置函数,并且仅在编译器不提供内置解决方案时才使用标准解决方案。
亚历克斯·雷肯

11

如果使用内联汇编器,则可以检查溢出标志。另一种可能性是可以使用safeint数据类型。我建议阅读有关Integer Security的本文。


6
+1这是另一种说法:“如果C不定义它,那么您将被迫进入特定于平台的行为。” 这么多在组装中容易处理的事情在C中是未定义的,以可移植性的名义在积雪之中创造了山脉。
Mike DeSimone 2010年

5
我对C问题的汇编答案不满意。就像我说过的那样,有一些正确,可移植的方法可以用C编写检查,这将生成与手工编写的完全相同的asm。自然地,如果使用它们,则对性能的影响将是相同的,并且与您也建议的C ++ safeint东西相比,其影响将小得多。
R .. GitHub停止帮助ICE,2010年

1
@Matthieu:如果编写的代码仅在一个实现中使用,并且该实现保证某些功能可以工作,并且需要良好的整数性能,则可以使用特定于实现的技巧。但是,这并不是OP所要求的。
David Thornley

3
C有充分的理由区分实现定义的行为和未定义的行为,即使UB在当前实现的版本中“起作用”,也不表示它将在以后的版本中继续起作用。考虑gcc和已签名的溢出行为...
R .. GitHub停止帮助ICE,2010年

2
由于我基于我-1一个要求,我们可以得到的C代码生成相同的ASM,我想这是唯一公平的收回它,当所有主要的编译器变成是在这方面的垃圾..
R.,GitHub的STOP HELPING ICE

6

最快的方法是使用内置的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

这比在32位计算机(使用gcc)上转换为64位类型要快得多:

    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

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;
}

您可能想仔细看看符号扩展在这里如何工作,但我认为这是正确的。


删除按位与,然后从return语句中进行强制转换。他们写错了。只要值适合较小的类型,就可以很好地定义从较大的有符号整数类型到较小的整数类型的转换,并且不需要显式强制转换。任何发出警告并建议您在仅检查该值不会溢出的情况下添加强制类型转换的编译器都是损坏的编译器。
R .. GitHub停止帮助ICE,2010年

@R你是正确的,我只喜欢露骨。不过,为了正确起见,我将其更改。对于将来的读者,返回行为return (int32_t)(sum & 0xffffffff);
乔纳森

2
请注意,如果您编写sum & 0xffffffffsum则会隐式转换为type unsigned int(假定为32位int),因为0xffffffff具有type unsigned int。然后,按位和的结果为unsigned int,如果sum为负,则将超出所支持的值的范围int32_t。到的转换将int32_t具有实现定义的行为。
R .. GitHub停止帮助ICE,2010年

请注意,这不适用于ints为64位的ILP64环境。
rtx13

1

怎么样:

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(对称或不); 该功能如图所示,但如何获得其他行为应该很明显)。


+1是一种更直观的替代方法。
R .. GitHub停止帮助ICE,2010年

1
我认为result = (n1 - INT_MAX)+n2;,如果n1小(例如0)且n2为负,则可能会溢出。
davmac 2013年

@davmac:嗯...也许有必要弄清楚三种情况:以for开头(n1 ^ n2) < 0,在二进制补码机器上这意味着值相反,可以直接加。如果值具有相同的符号,则上面给出的方法将是安全的。另一方面,我很好奇该标准的作者是否期望二进制补码静默溢出硬件的实现会在发生溢出的情况下跳出轨道,而这种方式不会立即导致程序异常终止,而是会导致其他计算的不可预测的中断。
超级猫

0

显而易见的解决方案是将其转换为unsigned,以获得定义明确的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编译器。


你还在把结果存储在sum,这是一个int。如果的值(unsigned)lhs + (unsigned)rhs大于,则会导致产生实施定义的结果或实施定义的信号INT_MAX
R .. GitHub停止帮助ICE,2010年

2
@R:这就是重点-行为是实现定义的,而不是未定义的,因此实现必须记录其行为并一致地进行。仅当实现记录了信号时,才可以引发信号,在这种情况下,必须始终引发信号,并且您可以使用该行为。
克里斯·多德

0

如果将两个long值相加,则可移植代码可以将long值分为上下两个int部分(或大小与相同的short部分):longint

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'

-1

我认为这可行:

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 for x86_64,此代码的程序集确实进行了加法,减法和测试,尽管它将所有内容存储在堆栈中以及不需要的堆栈操作。我什至试过了,register volatile int sum =但程序集是一样的。

对于仅具有int sum =(没有易失性或寄存器)版本的功能,该功能不进行测试,仅使用一条lea指令(lea即负载有效地址,通常用于在不触摸标志寄存器的情况下进行加法)进行加法。

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


4
-1用于滥用volatile掩盖未定义的行为。如果它“有效”,那么您仍然只是“幸运”。
R .. GitHub停止帮助ICE,2010年

@R:如果不起作用,编译器将无法volatile正确实现。我只想为一个已经解决的常见问题提供一个简单的解决方案。
nategoose 2010年

但是,可能失败的地方是一个系统,当整数溢出时,其数值表示会换为较低的值。
nategoose

最后的注释中应该有“没有”或“没有”。
nategoose 2010年

@nategoose,您的断言“如果编译器不起作用,则说明编译器无法正确实现volatile”是错误的。一方面,在二进制补码算法中,即使发生溢出,lhs = sum-rhs总是正确的。即使不是这种情况,尽管此特定示例有些人为设计,但编译器可能会生成例如执行加法,将结果值存储,将值读回另一个寄存器,将存储的值与读取的值进行比较的代码。值并注意它们是相同的,因此假定未发生溢出。
davmac 2013年

-1

对我来说,最简单的检查就是检查操作数的符号和结果。

让我们检查一下总和:仅当两个操作数具有相同的符号时,溢出才可能在+或-两个方向上发生。而且,很明显,当结果的符号与操作数的符号不同时,就会发生溢出。

因此,这样的检查就足够了:

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

编辑:如尼尔斯建议的,这是正确的if条件:

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

而自从当指令

add eax, ebx 

导致不确定的行为?英特尔x86指令集引用中没有这样的东西。


2
您在这里错过了重点。您的第二行代码sum = a + b可能会产生未定义的行为。
2010年

如果您在测试期间,除了投总和,a和b为无符号代码将BTW工作..
尼尔斯Pipenbrinck

它是未定义的,不是因为程序会崩溃或表现不同。处理器正在执行计算OF标志的确切操作。标准只是试图保护自己免受非标准情况的侵害,但这并不意味着您不能这样做。
ruslik 2010年

@Nils是的,我想这样做,但是我认为4(usngined int)会使它更加难以理解。(您知道,您首先阅读它,只有在喜欢时才尝试)。
ruslik 2010年

1
未定义的行为在C中,而不是在汇编后进行
phuclv
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.