由于内存损坏,整数溢出是否会导致未定义的行为?

时间:2016-05-19 13:59:21

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

我最近读到C和C ++中的带符号整数溢出会导致未定义的行为:

  

如果在评估表达式期间,结果未在数学上定义或未在其类型的可表示值范围内,则行为未定义。

我目前正在尝试了解未定义行为的原因。我认为这里发生了未定义的行为,因为当它变得太大而无法适应底层类型时,整数开始操纵内存本身。

所以我决定在Visual Studio 2015中编写一个小测试程序,用以下代码测试该理论:

#include <stdio.h>
#include <limits.h>

struct TestStruct
{
    char pad1[50];
    int testVal;
    char pad2[50];
};

int main()
{
    TestStruct test;
    memset(&test, 0, sizeof(test));

    for (test.testVal = 0; ; test.testVal++)
    {
        if (test.testVal == INT_MAX)
            printf("Overflowing\r\n");
    }

    return 0;
}

我在这里使用了一个结构来防止Visual Studio在调试模式下的任何保护问题,比如堆栈变量的临时填充等等。 无限循环应该导致test.testVal的几次溢出,确实如此,除了溢出本身之外没有任何后果。

我在运行溢出测试时查看了内存转储,结果如下(test.testVal的内存地址为0x001CFAFC):

0x001CFAE5  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x001CFAFC  94 53 ca d8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Overflowing integer with memory dump

如你所见,int周围的内存不断溢出仍然没有损坏&#34;。我用类似的输出测试了几次。从来没有任何关于溢出的int的内存损坏。

这里发生了什么?为什么变量test.testVal周围的内存没有受损?这怎么会导致未定义的行为?

我试图了解我的错误,以及为什么在整数溢出期间没有内存损坏。

6 个答案:

答案 0 :(得分:75)

您误解了未定义行为的原因。原因不是整数周围的内存损坏 - 它总是占据整数占用的相同大小 - 而是基础算术。

由于有符号整数不需要以2的补码进行编码,因此无法具体指导它们溢出时会发生什么。不同的编码或CPU行为可能导致不同的溢出结果,包括例如陷阱导致的程序杀死。

与所有未定义的行为一样,即使您的硬件对其算法使用2的补码并定义了溢出规则,编译器也不受它们的约束。例如,很长一段时间,GCC优化了任何只能在二进制补码环境中实现的检查。例如,if (x > x + 1) f()将从优化代码中删除,因为签名溢出是未定义的行为,这意味着它永远不会发生(从编译器的视图来看,程序永远不会包含产生未定义行为的代码),这意味着x永远不会大于x + 1

答案 1 :(得分:29)

标准的作者将整数溢出未定义,因为某些硬件平台可能会陷入其后果可能无法预测的方式(可能包括随机代码执行和随之而来的内存损坏)。尽管具有可预测的静音环绕溢出处理的二进制补充硬件在C89标准发布时已经成为标准(我已经检查了许多可重编程微机架构,零使用其他任何东西)标准的作者不希望阻止任何人在旧机器上生成C实现。

在实现普通的二进制补语静默环绕语义的实现上,代码如

int test(int x)
{
  int temp = (x==INT_MAX);
  if (x+1 <= 23) temp+=2;
  return temp;
}

将100%可靠地在传递INT_MAX值时返回3,因为添加 1到INT_MAX会产生INT_MIN,当然小于23。

在20世纪90年代,编译器使用了这样的事实:整数溢出是未定义的行为,而不是被定义为二进制补码包装,以实现各种优化,这意味着溢出的计算的确切结果将是不可预测的,但是不依赖于确切结果的行为将保持不变。给定上述代码的20世纪90年代的编译器可能会对其进行处理,就好像在INT_MAX中添加1会产生一个数值大于INT_MAX的值,从而导致函数返回1 而不是3,或者它可能表现得像旧的编译器,产生3.请注意,在上面的代码中,这种处理可以在许多平台上保存指令,因为(x + 1 <= 23)将等效于(x&lt; ; = 22)。编译器可以 在选择1或3时不一致,但生成的代码除了产生其中一个值之外不会做任何其他事情。

然而,从那以后,编译器使用它变得更加时髦 标准没有对程序行为施加任何要求 整数溢出(由硬件存在驱动的故障,其中 为了拥有编译器,可能会产生真正无法预测的后果 在溢出的情况下完全脱离轨道启动代码。现代编译器 可以注意到,如果x == INT_MAX,程序将调用Undefined Behavior, 从而得出结论,该函数永远不会传递该值。如果 函数永远不会传递该值,与INT_MAX的比较可以 省略。如果从另一个翻译单元调用上述功能 使用x == INT_MAX,它可能因此返回0或2;如果从同一个内部调用 翻译单元,自编译器以来,效果可能更奇怪 将关于x的推论扩展回调用者。

关于溢出是否会导致内存损坏,在某些旧硬件上可能会有。在现代硬件上运行的旧编译器上,它不会。在超现代编译器中,溢出否定了时间和因果关系的结构,所以所有的赌注都是关闭的。 x + 1评估中的溢出可以有效地破坏先前与INT_MAX的比较所看到的x的值,使其表现得好像内存中的x值已被破坏。此外,这种编译器行为通常会删除会阻止其他类型内存损坏的条件逻辑,从而允许发生任意内存损坏。

答案 2 :(得分:5)

未定义未定义的行为。它可能会使您的程序崩溃。它可能什么都不做。它可能完全符合您的预期。它可能会召唤鼻子恶魔。它可能会删除您的所有文件。当遇到未定义的行为时,编译器可以自由发出它喜欢的任何代码(或者根本不发出代码)。

任何未定义行为的实例都会导致整个程序未定义 - 不仅仅是未定义的操作,因此编译器可以对程序的任何部分执行任何操作。包括时间旅行: Undefined behavior can result in time travel (among other things, but time travel is the funkiest)

关于未定义的行为有很多答案和博客文章,但以下是我的最爱。如果您想了解有关该主题的更多信息,我建议您阅读它们。

答案 3 :(得分:5)

除了深奥的优化结果之外,你还必须考虑其他问题,即使是你天真地希望生成非优化编译器的代码。

  • 即使您知道该体系结构是二进制补码(或其他),溢出操作可能不会按预期设置标志,因此像if(a + b < 0)这样的语句可能会采用错误的分支:给定两个大的正数数字,所以当加在一起时它会溢出并且结果,所以二元补充纯粹主义者声称,是否定的,但加法指令可能实际上没有设置负面标志)

  • 多步操作可能发生在比sizeof(int)更宽的寄存器中,而不是在每一步都被截断,因此像(x << 5) >> 5这样的表达式可能不会切断左边的五位如你所愿,他们会。

  • 乘法和除法运算可以使用辅助寄存器来获取产品和股息中的额外位。如果乘法“不能”溢出,编译器可以自由地假设辅助寄存器为零(或负产品为-1)并且在分割之前不重置它。因此像x * y / z这样的表达式可能会使用比预期更广泛的中间产品。

其中一些听起来像是额外的准确性,但它是超出预期的额外准确性,无法预测或依赖,并且违反了您的心理模型“每个操作接受N位二进制补码操作数并返回下一个操作“

的结果的最低有效N位

答案 4 :(得分:5)

C ++标准未定义整数溢出行为。这意味着C ++的任何实现都可以随意做任何事情。

在实践中,这意味着:对于实现者来说最方便的是什么。由于大多数实现者将int视为二进制补码值,现在最常见的实现是说两个正数的溢出和是一个负数,它与真实结果有一定关系。这是错误答案,标准允许这样做,因为标准允许任何内容。

有一个论据要说integer overflow ought to be treated as an error,就像整数除零一样。 &#;; 86体系结构甚至有INTO指令在溢出时引发异常。在某些时候,该论点可能会获得足够的权重,使其成为主流编译器,此时整数溢出可能会导致崩溃。这也符合C ++标准,它允许实现做任何事情。

你可以想象一个架构,其中数字以小端方式表示为以空字符结尾的字符串,零字节表示&#34;数字结束&#34;。可以通过逐字节添加来完成添加,直到达到零字节。在这样的体系结构中,整数溢出可能会用一个覆盖尾随零,从而使得结果看起来更远,更长并且可能在将来破坏数据。这也符合C ++标准。

最后,正如其他一些回复所指出的那样,大量的代码生成和优化依赖于编译器对其生成的代码及其执行方式的推理。在整数溢出的情况下,编译器完全合法(a)生成用于添加的代码,其在添加大的正数时给出否定结果;以及(b)通过添加大的正数来知道其代码生成。给出了积极的结果。例如,

if (a+b>0) x=a+b;

可能,如果编译器知道ab都是肯定的,没有费心去执行测试,但无条件地将a添加到b并放置结果导致x。在二进制补码机器上,这可能导致负值被放入x,显然违反了代码的意图。这完全符合标准。

答案 5 :(得分:3)

未定义int表示的值。没有溢出&#39;在记忆中就像你想的那样。