由于内存损坏,整数溢出是否会导致未定义的行为?
Does integer overflow cause undefined behavior because of memory corruption?
我最近读到 C 和 C++ 中的有符号整数溢出会导致未定义的行为:
If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.
我目前正在尝试了解此处未定义行为的原因。我认为这里会发生未定义的行为,因为当整数变得太大而无法容纳基础类型时,整数开始操纵自身周围的内存。
所以我决定在 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
如你所见,不断溢出的int周围的内存仍然"undamaged"。我用类似的输出测试了几次。溢出的 int 周围的任何内存都没有损坏。
这里发生了什么?为什么变量 test.testVal
周围的内存没有损坏?这怎么会导致未定义的行为?
我想了解我的错误以及为什么在整数溢出期间没有发生内存损坏。
未定义 int
表示什么值。内存中没有你想的'overflow'
您误解了未定义行为的原因。原因不是整数周围的内存损坏——它总是占用与整数占用相同的大小——而是底层算法。
由于有符号整数不需要以 2 的补码进行编码,因此无法针对它们溢出时会发生什么情况提供具体指导。不同的编码或 CPU 行为会导致不同的溢出结果,例如,由于陷阱导致程序终止。
与所有未定义的行为一样,即使您的硬件对其算法使用 2 的补码并定义了溢出规则,编译器也不受它们的约束。例如,很长一段时间以来,GCC 优化掉了任何只能在 2 的补码环境中实现的检查。例如,if (x > x + 1) f()
将从优化代码中删除,因为有符号溢出是未定义的行为,这意味着它永远不会发生(从编译器的角度来看,程序永远不会包含产生未定义行为的代码),这意味着 x
永远不会大于 x + 1
.
未定义的行为是未定义的。它可能会使您的程序崩溃。它可能什么都不做。它可能完全符合您的预期。可能会召唤鼻魔。它可能会删除您的所有文件。当编译器遇到未定义的行为时,它可以自由地发出它喜欢的任何代码(或者 none)。
任何未定义行为的实例都会导致整个程序未定义 - 不仅仅是未定义的操作,因此编译器可以对程序的任何部分做任何它想做的事。包括时间旅行:Undefined behavior can result in time travel (among other things, but time travel is the funkiest).
有很多关于未定义行为的答案和博客文章,但以下是我最喜欢的。如果您想了解有关该主题的更多信息,我建议您阅读它们。
标准的作者未定义整数溢出,因为某些硬件平台可能陷入后果无法预测的方式(可能包括随机代码执行和随之而来的内存损坏)。尽管在 C89 标准发布时(在我检查过的许多可重新编程的微型计算机体系结构中,零使用其他任何东西),具有可预测的静默环绕溢出处理的二进制补码硬件已基本确立为标准,但标准的作者不想阻止任何人在旧机器上生成 C 实现。
在实现普通二进制补码无声环绕语义的实现中,代码如
int test(int x)
{
int temp = (x==INT_MAX);
if (x+1 <= 23) temp+=2;
return temp;
}
会,100% 可靠,return 3 当传递值 INT_MAX 时,因为添加
1 到 INT_MAX 会产生 INT_MIN,这当然小于 23.
在 1990 年代,编译器使用整数溢出是未定义行为这一事实,而不是被定义为二进制补码包装,以启用各种优化,这意味着溢出的计算的确切结果将无法预测,但不依赖于确切结果的行为方面将保留在 rails 上。给出上述代码的 1990 年代编译器可能会将其视为将 INT_MAX 加 1 产生的数值比 INT_MAX 大 1,从而导致函数 return 1
而不是 3,或者它可能表现得像旧的编译器,产生 3。请注意,在上面的代码中,这种处理可以在许多平台上节省一条指令,因为 (x+1 <= 23) 等同于 (x <= 22).编译器可能
在选择 1 或 3 时不一致,但生成的代码除了产生这些值之一外不会做任何事情。
然而,从那时起,编译器使用
在以下情况下,标准未能对程序行为施加任何要求
整数溢出(由硬件存在引起的故障,其中
结果可能真的是不可预测的)来证明有编译器是合理的
在溢出的情况下完全关闭 rails 启动代码。现代编译器
可以注意到,如果 x==INT_MAX,程序将调用未定义的行为,
从而得出结论,该函数永远不会传递该值。如果
函数永远不会传递那个值,与 INT_MAX 的比较可以是
省略。如果上述函数是从另一个翻译单元调用的
对于 x==INT_MAX,它可能因此 return 0 或 2;如果从同一个内部调用
翻译单元,效果可能会更奇怪,因为编译器会
将其关于 x 的推论扩展回调用者。
关于溢出是否会导致内存损坏,在一些老硬件上可能会有。在现代硬件上的旧编译器 运行 上,它不会。在超现代编译器上,溢出否定了时间和因果关系的结构,所以所有的赌注都没有了。 x+1 计算中的溢出可以有效地破坏之前与 INT_MAX 比较时看到的 x 值,使其表现得好像内存中的 x 值已被破坏。此外,此类编译器行为通常会删除本可防止其他类型的内存损坏的条件逻辑,从而允许发生任意内存损坏。
除了深奥的优化后果之外,您还必须考虑其他问题,即使是您天真地期望非优化编译器生成的代码也是如此。
即使您知道架构是二进制补码(或其他),溢出操作也可能不会按预期设置标志,因此像 if(a + b < 0)
这样的语句可能会采用错误的分支:给定两个大的正数,所以当它们加在一起时会溢出,并且结果,因此二进制补码纯粹主义者声称,是负数,但加法指令实际上可能不会设置负标志)
可能在比 sizeof(int) 更宽的寄存器中进行了多步操作,而没有在每一步都被截断,因此像 (x << 5) >> 5
这样的表达式可能不会被截断左边的五位如您所想。
乘法和除法运算可以使用辅助寄存器来存储乘积和被除数中的额外位。如果 multiply "can't" 溢出,编译器可以自由假设辅助寄存器为零(或 -1 对于负乘积)并且不会在除法之前将其重置。所以像 x * y / z
这样的表达式可能会使用比预期更宽的中间产品。
其中一些听起来像是额外的准确性,但它是意想不到的额外准确性,无法预测或依赖,并且违反了您的心智模型 "each operation accepts N-bit twos-complement operands and returns the least significant N bits of the result for the next operation"
C++ 标准未定义整数溢出行为。这意味着 C++ 的任何实现都可以随心所欲。
实际上这意味着:对实施者来说最方便的。并且由于大多数实现者将 int
视为二进制补码值,因此当今最常见的实现是说两个正数的溢出和是负数,与真实结果有某种关系。这是一个 错误的答案 并且它是标准允许的,因为标准允许任何东西。
有说法说integer overflow ought to be treated as an error,就像整数除以零一样。 '86 架构甚至有 INTO
指令来引发溢出异常。在某些时候,该论点可能会获得足够的权重以使其成为主流编译器,此时整数溢出可能会导致崩溃。这也符合 C++ 标准,它允许一个实现做任何事情。
您可以想象一种体系结构,其中数字以小尾数法表示为以 null 结尾的字符串,零字节表示 "end of number"。可以通过逐字节添加直到达到零字节来完成添加。在这样的体系结构中,整数溢出可能会用 1 覆盖尾随的 0,从而使结果看起来更长、更长,并可能在将来破坏数据。这也符合C++标准。
最后,正如其他一些回复所指出的,大量的代码生成和优化取决于编译器对其生成的代码及其执行方式的推理。在整数溢出的情况下,编译器 (a) 生成加法代码是完全合法的,这在加法大正数时会给出负结果,以及 (b) 通知其代码生成知道加法大正数给出了一个积极的结果。例如
if (a+b>0) x=a+b;
可能,如果编译器知道a
和b
都是正数,就懒得去测试了,而是无条件地把a
加到b
和将结果放入 x
。在二进制补码机器上,这可能会导致将负值放入 x
,这显然违反了代码的意图。这完全符合标准。
我最近读到 C 和 C++ 中的有符号整数溢出会导致未定义的行为:
If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.
我目前正在尝试了解此处未定义行为的原因。我认为这里会发生未定义的行为,因为当整数变得太大而无法容纳基础类型时,整数开始操纵自身周围的内存。
所以我决定在 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
如你所见,不断溢出的int周围的内存仍然"undamaged"。我用类似的输出测试了几次。溢出的 int 周围的任何内存都没有损坏。
这里发生了什么?为什么变量 test.testVal
周围的内存没有损坏?这怎么会导致未定义的行为?
我想了解我的错误以及为什么在整数溢出期间没有发生内存损坏。
未定义 int
表示什么值。内存中没有你想的'overflow'
您误解了未定义行为的原因。原因不是整数周围的内存损坏——它总是占用与整数占用相同的大小——而是底层算法。
由于有符号整数不需要以 2 的补码进行编码,因此无法针对它们溢出时会发生什么情况提供具体指导。不同的编码或 CPU 行为会导致不同的溢出结果,例如,由于陷阱导致程序终止。
与所有未定义的行为一样,即使您的硬件对其算法使用 2 的补码并定义了溢出规则,编译器也不受它们的约束。例如,很长一段时间以来,GCC 优化掉了任何只能在 2 的补码环境中实现的检查。例如,if (x > x + 1) f()
将从优化代码中删除,因为有符号溢出是未定义的行为,这意味着它永远不会发生(从编译器的角度来看,程序永远不会包含产生未定义行为的代码),这意味着 x
永远不会大于 x + 1
.
未定义的行为是未定义的。它可能会使您的程序崩溃。它可能什么都不做。它可能完全符合您的预期。可能会召唤鼻魔。它可能会删除您的所有文件。当编译器遇到未定义的行为时,它可以自由地发出它喜欢的任何代码(或者 none)。
任何未定义行为的实例都会导致整个程序未定义 - 不仅仅是未定义的操作,因此编译器可以对程序的任何部分做任何它想做的事。包括时间旅行:Undefined behavior can result in time travel (among other things, but time travel is the funkiest).
有很多关于未定义行为的答案和博客文章,但以下是我最喜欢的。如果您想了解有关该主题的更多信息,我建议您阅读它们。
标准的作者未定义整数溢出,因为某些硬件平台可能陷入后果无法预测的方式(可能包括随机代码执行和随之而来的内存损坏)。尽管在 C89 标准发布时(在我检查过的许多可重新编程的微型计算机体系结构中,零使用其他任何东西),具有可预测的静默环绕溢出处理的二进制补码硬件已基本确立为标准,但标准的作者不想阻止任何人在旧机器上生成 C 实现。
在实现普通二进制补码无声环绕语义的实现中,代码如
int test(int x)
{
int temp = (x==INT_MAX);
if (x+1 <= 23) temp+=2;
return temp;
}
会,100% 可靠,return 3 当传递值 INT_MAX 时,因为添加 1 到 INT_MAX 会产生 INT_MIN,这当然小于 23.
在 1990 年代,编译器使用整数溢出是未定义行为这一事实,而不是被定义为二进制补码包装,以启用各种优化,这意味着溢出的计算的确切结果将无法预测,但不依赖于确切结果的行为方面将保留在 rails 上。给出上述代码的 1990 年代编译器可能会将其视为将 INT_MAX 加 1 产生的数值比 INT_MAX 大 1,从而导致函数 return 1 而不是 3,或者它可能表现得像旧的编译器,产生 3。请注意,在上面的代码中,这种处理可以在许多平台上节省一条指令,因为 (x+1 <= 23) 等同于 (x <= 22).编译器可能 在选择 1 或 3 时不一致,但生成的代码除了产生这些值之一外不会做任何事情。
然而,从那时起,编译器使用 在以下情况下,标准未能对程序行为施加任何要求 整数溢出(由硬件存在引起的故障,其中 结果可能真的是不可预测的)来证明有编译器是合理的 在溢出的情况下完全关闭 rails 启动代码。现代编译器 可以注意到,如果 x==INT_MAX,程序将调用未定义的行为, 从而得出结论,该函数永远不会传递该值。如果 函数永远不会传递那个值,与 INT_MAX 的比较可以是 省略。如果上述函数是从另一个翻译单元调用的 对于 x==INT_MAX,它可能因此 return 0 或 2;如果从同一个内部调用 翻译单元,效果可能会更奇怪,因为编译器会 将其关于 x 的推论扩展回调用者。
关于溢出是否会导致内存损坏,在一些老硬件上可能会有。在现代硬件上的旧编译器 运行 上,它不会。在超现代编译器上,溢出否定了时间和因果关系的结构,所以所有的赌注都没有了。 x+1 计算中的溢出可以有效地破坏之前与 INT_MAX 比较时看到的 x 值,使其表现得好像内存中的 x 值已被破坏。此外,此类编译器行为通常会删除本可防止其他类型的内存损坏的条件逻辑,从而允许发生任意内存损坏。
除了深奥的优化后果之外,您还必须考虑其他问题,即使是您天真地期望非优化编译器生成的代码也是如此。
即使您知道架构是二进制补码(或其他),溢出操作也可能不会按预期设置标志,因此像
if(a + b < 0)
这样的语句可能会采用错误的分支:给定两个大的正数,所以当它们加在一起时会溢出,并且结果,因此二进制补码纯粹主义者声称,是负数,但加法指令实际上可能不会设置负标志)可能在比 sizeof(int) 更宽的寄存器中进行了多步操作,而没有在每一步都被截断,因此像
(x << 5) >> 5
这样的表达式可能不会被截断左边的五位如您所想。乘法和除法运算可以使用辅助寄存器来存储乘积和被除数中的额外位。如果 multiply "can't" 溢出,编译器可以自由假设辅助寄存器为零(或 -1 对于负乘积)并且不会在除法之前将其重置。所以像
x * y / z
这样的表达式可能会使用比预期更宽的中间产品。
其中一些听起来像是额外的准确性,但它是意想不到的额外准确性,无法预测或依赖,并且违反了您的心智模型 "each operation accepts N-bit twos-complement operands and returns the least significant N bits of the result for the next operation"
C++ 标准未定义整数溢出行为。这意味着 C++ 的任何实现都可以随心所欲。
实际上这意味着:对实施者来说最方便的。并且由于大多数实现者将 int
视为二进制补码值,因此当今最常见的实现是说两个正数的溢出和是负数,与真实结果有某种关系。这是一个 错误的答案 并且它是标准允许的,因为标准允许任何东西。
有说法说integer overflow ought to be treated as an error,就像整数除以零一样。 '86 架构甚至有 INTO
指令来引发溢出异常。在某些时候,该论点可能会获得足够的权重以使其成为主流编译器,此时整数溢出可能会导致崩溃。这也符合 C++ 标准,它允许一个实现做任何事情。
您可以想象一种体系结构,其中数字以小尾数法表示为以 null 结尾的字符串,零字节表示 "end of number"。可以通过逐字节添加直到达到零字节来完成添加。在这样的体系结构中,整数溢出可能会用 1 覆盖尾随的 0,从而使结果看起来更长、更长,并可能在将来破坏数据。这也符合C++标准。
最后,正如其他一些回复所指出的,大量的代码生成和优化取决于编译器对其生成的代码及其执行方式的推理。在整数溢出的情况下,编译器 (a) 生成加法代码是完全合法的,这在加法大正数时会给出负结果,以及 (b) 通知其代码生成知道加法大正数给出了一个积极的结果。例如
if (a+b>0) x=a+b;
可能,如果编译器知道a
和b
都是正数,就懒得去测试了,而是无条件地把a
加到b
和将结果放入 x
。在二进制补码机器上,这可能会导致将负值放入 x
,这显然违反了代码的意图。这完全符合标准。