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

我最近读到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("Overflowingrn");
    }

    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

溢出的整数与内存转储

正如你所看到的,不断溢出的int内存保持“完好无损”。 我用相似的输出测试了几次。 从来没有任何内存周围的溢出int损坏。

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

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


你误解了未定义行为的原因。 原因不是整数周围的内存破坏 - 它将始终占用整数占据的相同大小 - 而是底层算术。

由于有符号整数不需要用2的补码来编码,所以当它们溢出时将不会有什么特别的指导。 不同的编码或CPU行为会导致不同的溢出结果,例如,由于陷阱而导致的程序死机。

和所有未定义的行为一样,即使您的硬件使用2的补码进行算术运算,并且定义了溢出规则,编译器也不受它们的束缚。 例如,很长时间以来,GCC对任何仅在2的补码环境中实现的检查进行了优化。 例如, if (x > x + 1) f()将从优化代码中移除,因为有符号溢出是未定义行为,意味着它永远不会发生(从编译器的角度看,程序从不包含产生未定义行为的代码),这意味着x永远不会大于x + 1


该标准的作者左整数溢出未定义,因为某些硬件平台可能陷入后果可能不可预知(可能包括随机代码执行和随之而来的内存损坏)的方式。 尽管在C89标准发布之前(尽管我已经研究过许多可重新编程的微型计算机体系结构,零其他任何地方都没有使用),但具有可预测的无声环绕溢出处理的二进制补充硬件几乎已经成为标准。并不想阻止任何人在旧机器上生成C实现。

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

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

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

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

然而,从那时起,对于编译器来说,使用标准的失败对程序行为施加任何要求,在整数溢出的情况下(由于存在硬件而导致后果可能是真正不可预知的)在溢出的情况下完全离开轨道启动代码。 一个现代的编译器可能会注意到,如果x == INT_MAX,程序将调用未定义的行为,并因此推断该函数永远不会传递该值。 如果函数永远不会传递该值,则可以省略与INT_MAX的比较。 如果上述函数是从另一个x == INT_MAX的翻译单元调用的,则它可能因此返回0或2; 如果从同一个翻译单元中调用,效果可能会更加奇怪,因为编译器会将其对x的推论扩展回调用者。

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


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

未定义行为的任​​何实例都会导致整个程序未定义 - 不仅仅是未定义的操作,所以编译器可以对程序的任何部分执行任何操作。 包括时间旅行:未定义的行为会导致时间旅行(除其他外,但时间旅行是最简单的)。

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

  • C和C ++中未定义行为指南,第1部分
  • 每个C程序员应该知道的未定义行为#1/3
  • 链接地址: http://www.djcxy.com/p/79925.html

    上一篇: Does integer overflow cause undefined behavior because of memory corruption?

    下一篇: How to detect integer overflow?