在C/ c++中没有有效和可靠的方法来检测整数溢出

No useful and reliable way to detect integer overflow in C/C++?

本文关键字:方法 检测 溢出 整数 c++ 有效      更新时间:2023-10-16

不,这不是如何检测整数溢出?的重复。问题是一样的,但问题是不同的。


gcc编译器可以优化掉溢出检查(使用-O2),例如:

int a, b;
b = abs(a);                     // will overflow if a = 0x80000000
if (b < 0) printf("overflow");  // optimized away

gcc的人认为这不是一个bug。根据C标准,溢出是未定义的行为,它允许编译器执行任何操作。显然,任何都包含了永远不会发生溢出的假设。不幸的是,这允许编译器优化掉溢出检查。

在最近的一篇CERT论文中描述了检查溢出的安全方法。本文建议在添加两个整数之前执行以下操作:

if ( ((si1^si2) | (((si1^(~(si1^si2) & INT_MIN)) + si2)^si2)) >= 0) { 
  /* handle error condition */
} else {
  sum = si1 + si2;
}

显然,当您想要确保结果有效时,必须在一系列计算中的每个+、-、*、/和其他操作之前执行类似的操作。例如,如果您想确保数组索引不越界。这太麻烦了,实际上没人做。至少我从未见过一个C/c++程序系统地做到这一点。

现在,这是一个基本问题:

  • 在访问数组之前检查数组索引是有用的,但不可靠。

  • 用CERT方法检查一系列计算中的每个操作是可靠的,但没有用。

  • 结论:在C/c++中没有有效和可靠的方法来检查溢出!

我拒绝相信这是标准编写时的意图。

我知道有一些命令行选项可以解决这个问题,但这并不能改变这样一个事实,即我们对标准或当前对标准的解释存在根本问题。

我的问题是:当gcc人员允许他们优化溢出检查时,是对"未定义行为"的解释走得太远了,还是C/c++标准被破坏了?

添加注:对不起,你可能误解了我的问题。我不是在问如何解决这个问题——这个问题已经在其他地方得到了解答。我在问一个关于C标准的更基本的问题。如果没有有用和可靠的方法来检查溢出,那么语言本身就是可疑的。例如,如果我创建了一个带有边界检查的安全数组类,那么我应该是安全的,但如果边界检查可以优化掉,我就不安全了。

如果标准允许这种情况发生,那么要么标准需要修订,要么标准的解释需要修订。

新增注释2:这里的人们似乎不愿意讨论"未定义行为"这个可疑的概念。事实上,C99标准列出了191种不同的未定义行为(链接),这表明这是一个草率的标准。

许多程序员欣然接受"未定义行为"这一说法,即允许他们做任何事情,包括格式化硬盘。我认为这是一个问题,标准将整数溢出写入与数组边界外相同的危险类别。

为什么这两种"未定义行为"不同?因为:

  • 许多程序依赖于整数溢出是良性的,但很少有程序依赖于在不知道存在什么的情况下在数组边界外写入。

  • 实际上可以做一些糟糕的格式化你的硬盘(至少在一个不受保护的操作系统,如DOS),大多数程序员都知道这是危险的

  • 当你把整型溢出放入危险的"anything goes"类别时,它允许编译器做任何事情,包括对它正在做的事情撒谎(在溢出检查被优化掉的情况下)

  • 可以通过调试器发现写入数组边界以外的错误,但是无法通过优化取消溢出检查的错误,因为在调试时优化通常是关闭的。

  • 在整数溢出的情况下,gcc编译器显然避免了"任意"策略。在许多情况下,它会避免优化,例如,除非它可以验证溢出是不可能的。由于某种原因,gcc的人已经认识到,如果他们遵循"一切正常"的策略,我们将会有太多的错误,但是他们对优化消除溢出检查的问题有不同的态度。

也许这不是讨论这种哲学问题的合适地方。至少,这里的大多数答案都离题了。有更好的地方讨论这个吗?

gcc开发人员在这里是完全正确的。当标准说行为是未定义的,这意味着在编译器上没有要求。

由于一个有效的程序不能做任何导致UB的事情(因为它将不再有效),编译器可以很好地假设UB没有发生。如果它仍然存在,编译器所做的任何事情都是可以的。

对于溢出问题,一个解决方案是考虑计算应该处理的范围。例如,在平衡我的银行账户时,我可以假设金额远低于10亿,因此32位整型可以工作。

对于您的应用程序域,您可能可以对可能溢出的位置进行类似的估计。然后,您可以在这些点添加检查,或者选择其他数据类型(如果可用)。

int a, b;
b = abs(a); // will overflow if a = 0x80000000
if (b < 0) printf("overflow");  // optimized away 

(你似乎在假设2s互补…让我们运行一下)

如果a具有二进制模式(更准确地说,如果aINT_MIN),谁说abs(a)"溢出"?abs(int)的Linux手册页说:

尝试取最大负整数的绝对值是没有定义的。

未定义并不一定意味着溢出。

所以,你的前提b可以小于0,这在某种程度上是对"溢出"的测试,从一开始就是有根本缺陷的。如果你想测试,你不能在可能有未定义行为的结果上做测试——而是在操作之前做测试!

如果你关心这一点,你可以使用c++的用户定义类型(即类)来实现你自己的一组测试,围绕你需要的操作(或者找到一个已经这样做的库)。该语言不需要为此提供内置支持,因为它可以在这样的库中同样有效地实现,而使用的结果语义不变。这是c++最重要的功能之一。

问问你自己:你到底多久需要校验过的算术?如果经常需要,应该编写一个checked_int类,重载通用操作符并将检查封装到该类中。在开源网站上分享实现的道具。

更好的是(有争议的),使用big_integer类,这样首先就不会发生溢出。

请使用正确的b类型:

int a;
unsigned b = a;
if (b == (unsigned)INT_MIN) printf("overflow");  // never optimized away
else b = abs(a);

Edit: C中的溢出测试可以安全地使用unsigned类型完成。无符号类型只是绕在算术上,有符号类型可以安全地转换为它们。所以你可以对它们做任何你喜欢的测试。在现代处理器上,这种转换通常只是对寄存器的重新解释,所以它不需要运行时成本。