【问题标题】:No useful and reliable way to detect integer overflow in C/C++?没有有用且可靠的方法来检测 C/C++ 中的整数溢出?
【发布时间】:2011-10-14 22:26:06
【问题描述】:

不,这不是 How to detect integer overflow? 的副本。问题相同,但问题不同。


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

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

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

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

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++ 标准被破坏了?

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

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

添加注释 2: 这里的人们似乎不愿意讨论“未定义行为”这个可疑的概念。 C99 标准列出了 191 种不同类型的未定义行为 (link),这表明标准草率。

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

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

  • 许多程序依赖整数溢出是良性的,但是当你不知道那里有什么时,很少有程序依赖于写入外部数组边界。

  • 在数组边界外写入实际上可以做一些与格式化硬盘一样糟糕的事情(至少在像 DOS 这样不受保护的操作系统中),并且大多数程序员都知道这很危险。 p>

  • 当您将整数溢出置于危险的“任何事情发生”类别时,它允许编译器做任何事情,包括谎报正在做的事情(在溢出检查被优化的情况下)

  • 使用调试器可以发现写入超出数组边界之类的错误,但优化掉溢出检查的错误却不能,因为调试时通常会关闭优化。

  • gcc 编译器显然会避免在整数溢出的情况下使用“一切正常”的策略。在许多情况下,它会避免优化,例如一个循环,除非它可以验证溢出是不可能的。出于某种原因,gcc 的人已经认识到,如果他们在这里遵循“随便什么”的政策,我们将会有太多的错误,但是他们对优化掉溢出检查的问题有不同的态度。

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

【问题讨论】:

  • 你在这里混淆了两个根本不同的东西——数组溢出检查,除了调试之外很少有用,还有算术溢出检查,经常有用。
  • “我拒绝相信”——这是你的决定,但如果你拒绝相信真实的事情,那么你就会遇到困难。有符号整数类型在 C 或 C++ 中对于可能溢出的计算没有用处。这是该标准旨在避免对“自然”行为与您或我认为的行为不同的机器架构施加开销的结果。为避免实施负担,这种后果被认为是值得付出的。
  • 具有数组边界的计算尤其适用于无符号算术,因为您知道,无论如何您都不应该得到负的最终结果。
  • “如果没有有用且可靠的方法来检查溢出,那么语言本身就是可疑的”——那么根据您的定义,该语言是可疑的。如果您发现该语言没有用,请不要使用它(或仅使用提供有关溢出的额外保证的实现和命令行选项)。您与 C 标准委员会分道扬镳的地方可能是您认为在操作之前检查溢出是无用的,而他们认为不是。可能你也认为无符号类型是无用的,而他们没有。

标签: c++ c overflow integer-overflow gcc4


【解决方案1】:

问问自己:您实际上多久需要一次检查算术?如果您经常需要它,您应该编写一个checked_int 类来重载常用运算符并将检查封装到此类中。在开源网站上分享实现的道具。

更好的是(可以说),使用big_integer 类,这样一开始就不会发生溢出。

【讨论】:

    【解决方案2】:

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

    由于一个有效的程序不能做任何导致 UB 的事情(因为那样它就不再有效了),编译器可以很好地假设 UB 不会发生。如果它仍然如此,那么编译器所做的任何事情都可以。

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

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

    【讨论】:

    • “例如,在平衡我的银行账户时,我可以假设金额将远低于 10 亿,因此 32 位 int 将起作用。” - 并且有一个优势,在恶性通货膨胀的情况下,你可以保证有一份工作来修复你自己的代码。你真的不想在恶性通货膨胀期间失业。
    • 这种假设正是导致 Y2K 问题的原因。
    • @Graham - 不,不是。如果您进行数值计算,您必须知道应用程序的预期域。如果域发生变化,您将不得不重新评估。还是您已经在为 Y10K 问题做准备?
    • 这是真的。这种想法,“我将使用一个固定大小的类型/字段,因为它更快并且永远不会因内存不足而失败,并且只要我处理的数字范围是有限的”,这种假设就意味着 Y2k , Y10k 和 1000% 的通货膨胀都会导致软件问题。 Y2k 和 Y10k 问题之间的区别只是在 60 年代,当人们开始编写大量商业软件时,他们并不知道他们的软件,并且它的约定仍然在 Y2K 中使用。现在我们知道它是,但不知道我们的软件和约定是否会在 Y10K 中使用。
    • 哦,另一个区别是我更有信心我会在 8000 年后死去,而不是他们应该在 40 年后死去。因此,即使我和他们一样错。
    【解决方案3】:
    int a, b;
    b = abs(a); // will overflow if a = 0x80000000
    if (b < 0) printf("overflow");  // optimized away 
    

    (您似乎在假设 2s 补码......让我们一起运行吧)

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

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

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

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

    如果您关心这一点,您可以使用 C++ 的用户定义类型(即类)围绕您需要的操作实现您自己的一组测试(或找到一个已经这样做的库)。该语言不需要对此的内置支持,因为它可以在这样的库中同样有效地实现,而使用的结果语义不变。这种基本力量是 C++ 的一大优点。

    【讨论】:

      【解决方案4】:

      只需为b 使用正确的类型:

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

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

      【讨论】:

      • 我以前从未听说过UINT_MIN - 那不只是0吗?
      • 确实没有意义,而且演员阵容也很奇怪。此外,这可能不是预期的测试。导致溢出的 2s 补码 INT_MIN 将等于 UINT_MAX/2 + 1。所以我认为预期的测试是if (b==unsigned(INT_MAX)。但为什么不是更清晰的if (a==INT_MAX) 呢?
      • @A Fog,C 中的溢出测试可以使用unsigned 类型安全地完成。无符号类型只是在算术上环绕,有符号类型可以安全地转换为它们。因此,您可以对它们进行任何您喜欢的测试。在现代处理器上,这种转换通常只是对寄存器的重新解释,所以它是免费的。
      • @Kerrek,@MSalters,INT_MIN,确实如此。我只是按照 OP 放置它的方式留下了它,以显示溢出测试的位置。
      • 同意。在 C/C++ 中检查溢出的正确方法是无符号算术。将其指定为标准包装是有原因的!
      猜你喜欢
      • 1970-01-01
      • 2012-01-21
      • 2015-04-23
      • 1970-01-01
      • 1970-01-01
      • 2021-11-10
      • 2019-08-23
      • 2021-03-15
      相关资源
      最近更新 更多