编译:
#include <iostream>
int main()
{
for (int i = 0; i < 4; ++i)
std::cout << i*1000000000 << std::endl;
}
和gcc
会产生以下警告:
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
我知道有一个带符号的整数溢出。
我无法得到的是为什么i
值被溢出操作破坏了?
我已经阅读了Why does integer overflow on x86 with GCC cause an infinite loop?的答案,但我仍然不清楚为什么这种情况发生了 - 我明白了#34;未定义&#34;意味着&#34;任何事情都可能发生&#34;,但这种特定行为的根本原因是什么?
编译器:gcc (4.8)
答案 0 :(得分:103)
有符号整数溢出(严格来说,没有“无符号整数溢出”)意味着未定义行为。这意味着任何事情都可能发生,并且讨论为什么它会在C ++规则下发生没有意义。
C ++ 11草案N3337:§5.4: 1
如果在评估表达式期间,结果在数学上没有定义或在范围内没有 其类型的可表示值,行为未定义。 [注意:大多数现有的C ++实现 忽略整数而不是flows。除以零的处理,使用零除数形成余数,以及所有 浮动点异常因机器而异,通常可通过库函数进行调整。 - 后注]
使用g++ -O3
编译的代码会发出警告(即使没有-Wall
)
a.cpp: In function 'int main()':
a.cpp:11:18: warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
a.cpp:9:2: note: containing loop
for (int i = 0; i < 4; ++i)
^
我们分析程序正在做什么的唯一方法是读取生成的汇编代码。
以下是完整的装配清单:
.file "a.cpp"
.section .text$_ZNKSt5ctypeIcE8do_widenEc,"x"
.linkonce discard
.align 2
LCOLDB0:
LHOTB0:
.align 2
.p2align 4,,15
.globl __ZNKSt5ctypeIcE8do_widenEc
.def __ZNKSt5ctypeIcE8do_widenEc; .scl 2; .type 32; .endef
__ZNKSt5ctypeIcE8do_widenEc:
LFB860:
.cfi_startproc
movzbl 4(%esp), %eax
ret $4
.cfi_endproc
LFE860:
LCOLDE0:
LHOTE0:
.section .text.unlikely,"x"
LCOLDB1:
.text
LHOTB1:
.p2align 4,,15
.def ___tcf_0; .scl 3; .type 32; .endef
___tcf_0:
LFB1091:
.cfi_startproc
movl $__ZStL8__ioinit, %ecx
jmp __ZNSt8ios_base4InitD1Ev
.cfi_endproc
LFE1091:
.section .text.unlikely,"x"
LCOLDE1:
.text
LHOTE1:
.def ___main; .scl 2; .type 32; .endef
.section .text.unlikely,"x"
LCOLDB2:
.section .text.startup,"x"
LHOTB2:
.p2align 4,,15
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB1084:
.cfi_startproc
leal 4(%esp), %ecx
.cfi_def_cfa 1, 0
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
.cfi_escape 0x10,0x5,0x2,0x75,0
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
pushl %ecx
.cfi_escape 0xf,0x3,0x75,0x70,0x6
.cfi_escape 0x10,0x7,0x2,0x75,0x7c
.cfi_escape 0x10,0x6,0x2,0x75,0x78
.cfi_escape 0x10,0x3,0x2,0x75,0x74
xorl %edi, %edi
subl $24, %esp
call ___main
L4:
movl %edi, (%esp)
movl $__ZSt4cout, %ecx
call __ZNSolsEi
movl %eax, %esi
movl (%eax), %eax
subl $4, %esp
movl -12(%eax), %eax
movl 124(%esi,%eax), %ebx
testl %ebx, %ebx
je L15
cmpb $0, 28(%ebx)
je L5
movsbl 39(%ebx), %eax
L6:
movl %esi, %ecx
movl %eax, (%esp)
addl $1000000000, %edi
call __ZNSo3putEc
subl $4, %esp
movl %eax, %ecx
call __ZNSo5flushEv
jmp L4
.p2align 4,,10
L5:
movl %ebx, %ecx
call __ZNKSt5ctypeIcE13_M_widen_initEv
movl (%ebx), %eax
movl 24(%eax), %edx
movl $10, %eax
cmpl $__ZNKSt5ctypeIcE8do_widenEc, %edx
je L6
movl $10, (%esp)
movl %ebx, %ecx
call *%edx
movsbl %al, %eax
pushl %edx
jmp L6
L15:
call __ZSt16__throw_bad_castv
.cfi_endproc
LFE1084:
.section .text.unlikely,"x"
LCOLDE2:
.section .text.startup,"x"
LHOTE2:
.section .text.unlikely,"x"
LCOLDB3:
.section .text.startup,"x"
LHOTB3:
.p2align 4,,15
.def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef
__GLOBAL__sub_I_main:
LFB1092:
.cfi_startproc
subl $28, %esp
.cfi_def_cfa_offset 32
movl $__ZStL8__ioinit, %ecx
call __ZNSt8ios_base4InitC1Ev
movl $___tcf_0, (%esp)
call _atexit
addl $28, %esp
.cfi_def_cfa_offset 4
ret
.cfi_endproc
LFE1092:
.section .text.unlikely,"x"
LCOLDE3:
.section .text.startup,"x"
LHOTE3:
.section .ctors,"w"
.align 4
.long __GLOBAL__sub_I_main
.lcomm __ZStL8__ioinit,1,1
.ident "GCC: (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.0"
.def __ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef
.def __ZNSolsEi; .scl 2; .type 32; .endef
.def __ZNSo3putEc; .scl 2; .type 32; .endef
.def __ZNSo5flushEv; .scl 2; .type 32; .endef
.def __ZNKSt5ctypeIcE13_M_widen_initEv; .scl 2; .type 32; .endef
.def __ZSt16__throw_bad_castv; .scl 2; .type 32; .endef
.def __ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef
.def _atexit; .scl 2; .type 32; .endef
我甚至几乎无法阅读汇编,但即使我可以看到addl $1000000000, %edi
行。
结果代码看起来更像
for(int i = 0; /* nothing, that is - infinite loop */; i += 1000000000)
std::cout << i << std::endl;
@ T.C。的评论:
我怀疑它类似于:(1)因为任何大于2的值
i
的每次迭代都有未定义的行为 - &gt; (2)我们可以假设i <= 2
用于优化目的 - &gt; (3)循环条件总是正确的 - &gt; (4)它被优化成无限循环。
让我想到将OP代码的汇编代码与以下代码的汇编代码进行比较,没有未定义的行为。
#include <iostream>
int main()
{
// changed the termination condition
for (int i = 0; i < 3; ++i)
std::cout << i*1000000000 << std::endl;
}
事实上,正确的代码有终止条件。
; ...snip...
L6:
mov ecx, edi
mov DWORD PTR [esp], eax
add esi, 1000000000
call __ZNSo3putEc
sub esp, 4
mov ecx, eax
call __ZNSo5flushEv
cmp esi, -1294967296 // here it is
jne L7
lea esp, [ebp-16]
xor eax, eax
pop ecx
; ...snip...
OMG,这完全不明显!这不公平!我要求用火试验!
处理它,你编写了错误的代码,你应该感觉很糟糕。承担后果。
......或者,正确使用更好的诊断和更好的调试工具 - 这就是它们的用途:
启用所有警告
-Wall
是gcc选项,可以启用所有有用的警告而不会出现误报。这是您应该经常使用的最低限度。-Wall
,因为他们可能会对误报发出警告使用调试标志进行调试
-ftrapv
在溢出时捕获程序,-fcatch-undefined-behavior
捕获大量未定义行为的实例(注意:"a lot of" != "all of them"
)我有一个不是我写的程序意大利面,需要明天发货! HELP !!!!!! 111oneone
使用gcc的-fwrapv
该选项指示编译器假设加法,减法和乘法的有符号算术溢出使用二进制补码表示。
1 - 此规则不适用于“无符号整数溢出”,如§3.9.1.4所述
无符号整数,声明无符号整数,应遵守算术模2 n 的定律,其中n是数字 特定大小整数的值表示中的位数。
和UINT_MAX + 1
的结果在数学上被定义 - 由算术模2的规则 n
答案 1 :(得分:64)
简短回答,gcc
专门记录了这个问题,我们可以在gcc 4.8 release notes中看到(强调我的前进):
GCC现在使用更强大的积极分析来获得上限 使用约束强加的循环迭代次数 语言标准。这可能导致不合格的程序为否 按预期工作的时间更长,例如SPEC CPU 2006 464.h264ref和 416.gamess。添加了一个新选项-fno-aggressive-loop-optimizations以禁用此积极分析。在一些循环中 已知的常数迭代次数,但已知未定义的行为 在到达或在最后一次迭代期间,在GCC中发生 将警告循环中的未定义行为而不是派生 循环迭代次数的下限。该 可以使用-Wno-aggressive-loop-optimizations禁用警告。
而且如果我们使用-fno-aggressive-loop-optimizations
,无限循环行为应该停止,并且在我测试的所有情况下都会这样做。
通过查看草案C ++标准部分5
表达式段 4 <,知道有符号整数溢出是未定义的行为,这个答案很长/ em>其中说:
如果在评估表达式时,结果不是 在数学上定义或不在可表示值的范围内 它的类型,行为未定义。 [注:大多数现有 C ++的实现忽略整数溢出。治疗师 零,使用零除数形成余数,并且全部浮动 点异常因机器而异,通常可通过a调整 库函数。 - 注意
我们知道该标准表示未定义的行为是不可预测的,因为该定义附带的说明如下:
[注意:本国际时可能会出现未定义的行为 标准省略了行为或程序的任何明确定义 使用错误的构造或错误的数据。 允许未定义 行为的范围从完全忽略情况 在翻译或节目期间表现不可预测的结果 以文件化的方式执行环境特征 (有或没有发出诊断信息),终止 翻译或执行(发布诊断 信息)。许多错误的程序结构不会产生未定义的 行为;他们需要被诊断出来。 - 后注]
但是gcc
优化器在世界上可以做些什么来将其转变为无限循环?听起来很古怪。但幸运的是,gcc
为我们提供了在警告中找出它的线索:
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
线索是Waggressive-loop-optimizations
,这是什么意思?幸运的是,对于我们来说,这并不是第一次这种优化以这种方式破坏代码而且我们很幸运,因为 John Regehr 在文章GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks中记录了一个案例,其中显示了以下代码: / p>
int d[16];
int SATD (void)
{
int satd = 0, dd, k;
for (dd=d[k=0]; k<16; dd=d[++k]) {
satd += (dd < 0 ? -dd : dd);
}
return satd;
}
文章说:
未定义的行为是在退出之前访问d [16] 环。在C99中,创建指向元素1的指针是合法的 位于数组末尾的位置,但该指针不得为 解除引用。
后来说:
详细说明,这是正在发生的事情。一个C编译器,看到d [++ k], 允许假设k的增量值在 数组边界,因为否则会发生未定义的行为。对于代码 在这里, GCC可以推断出k在0..15的范围内。稍后,什么时候 海湾合作委员会认为k <16,它对自己说:“啊哈 - 表达总是如此 是的,所以我们有一个无限循环。“这里的情况,在哪里 编译器使用明确定义的假设来推断有用的 数据流事实,
因此编译器在某些情况下必须做的是假设由于有符号整数溢出是未定义的行为,因此i
必须始终小于4
,因此我们有一个无限循环。
他解释说,这与臭名昭着的Linux kernel null pointer check removal非常相似,在这里看到这段代码:
struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;
gcc
推断,由于s
中的s->f;
已被引用,并且由于取消引用空指针是未定义的行为,因此s
不能为空,因此会优化if (!s)
1}}检查下一行。
这里的教训是,现代优化器在利用未定义的行为方面非常积极,而且很可能只会变得更具侵略性。显然,只有几个例子,我们可以看到优化器对程序员做了一些看起来完全不合理的事情,但从优化器的角度回顾是有道理的。
答案 2 :(得分:23)
tl; dr 代码生成的测试整数 + 正整数 == 负整数。通常,优化器不会对此进行优化,但在接下来使用std::endl
的特定情况下,编译器会优化此测试。我还没弄清楚endl
还有什么特别之处。
从-O1及更高级别的汇编代码中,很明显gcc将循环重构为:
i = 0;
do {
cout << i << endl;
i += NUMBER;
}
while (i != NUMBER * 4)
正确运行的最大值是715827882
,即楼层(INT_MAX/3
)。 -O1
处的汇编代码段为:
L4:
movsbl %al, %eax
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
addl $715827882, %esi
cmpl $-1431655768, %esi
jne L6
// fallthrough to "return" code
注意,-1431655768
在{2}补码中为4 * 715827882
。
点击-O2
将其优化为以下内容:
L4:
movsbl %al, %eax
addl $715827882, %esi
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
cmpl $-1431655768, %esi
jne L6
leal -8(%ebp), %esp
jne L6
// fallthrough to "return" code
因此,所做的优化仅仅是addl
向上移动。
如果我们使用715827883
重新编译,那么除了更改的数字和测试值之外,-O1版本是相同的。然而,-O2然后做出改变:
L4:
movsbl %al, %eax
addl $715827883, %esi
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
jmp L2
cmpl $-1431655764, %esi
处有-O1
的位置,-O2
已删除该行。优化程序必须已决定将715827883
添加到%esi
永远不能等于-1431655764
。
这非常令人费解。将其添加到INT_MIN+1
会生成预期结果,因此优化程序必须已决定%esi
永远不会INT_MIN+1
,我不知道为什么会这样决定。
在工作示例中,似乎同样有效的结论是,将715827882
添加到数字不能等于INT_MIN + 715827882 - 2
! (这只有在实际发生环绕时才可能),但它不会优化该示例中的线路。
我使用的代码是:
#include <iostream>
#include <cstdio>
int main()
{
for (int i = 0; i < 4; ++i)
{
//volatile int j = i*715827883;
volatile int j = i*715827882;
printf("%d\n", j);
std::endl(std::cout);
}
}
如果删除std::endl(std::cout)
,则不再进行优化。实际上,用std::cout.put('\n'); std::flush(std::cout);
替换它也会导致优化不会发生,即使内联std::endl
也是如此。
std::endl
的内联似乎会影响循环结构的早期部分(我不太明白它在做什么,但我会在这里发布以防其他人这样做):
使用原始代码和-O2
:
L2:
movl %esi, 28(%esp)
movl 28(%esp), %eax
movl $LC0, (%esp)
movl %eax, 4(%esp)
call _printf
movl __ZSt4cout, %eax
movl -12(%eax), %eax
movl __ZSt4cout+124(%eax), %ebx
testl %ebx, %ebx
je L10
cmpb $0, 28(%ebx)
je L3
movzbl 39(%ebx), %eax
L4:
movsbl %al, %eax
addl $715827883, %esi
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
jmp L2 // no test
通过mymanual内联std::endl
,-O2
:
L3:
movl %ebx, 28(%esp)
movl 28(%esp), %eax
addl $715827883, %ebx
movl $LC0, (%esp)
movl %eax, 4(%esp)
call _printf
movl $10, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl $__ZSt4cout, (%esp)
call __ZNSo5flushEv
cmpl $-1431655764, %ebx
jne L3
xorl %eax, %eax
这两者之间的一个区别是原始版本使用%esi
,第二版版本使用%ebx
;一般来说%esi
和%ebx
之间定义的语义是否存在差异? (我对x86汇编不太了解。)
答案 3 :(得分:6)
我无法得到的是为什么我的值被溢出操作打破了?
似乎整数溢出发生在第4次迭代中(对于i = 3
)。
signed
整数溢出调用未定义的行为。在这种情况下,无法预测任何事情。循环可能只迭代4
次,或者它可能会变为无限或其他任何东西!
结果可能会因编译器与编译器的不同而不同,甚至可能因同一编译器
本国际标准没有要求的行为
[注意:当本国际标准忽略任何明确的行为定义或程序使用错误的构造或错误数据时,可能会出现未定义的行为。 允许的未定义行为包括完全忽略不可预测的结果,在转换或程序执行期间以环境特征(有或没有发出诊断消息)的文档方式执行,终止翻译或执行(发布诊断信息。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。 - 后注]
答案 4 :(得分:6)
在gcc中报告此错误的另一个例子是当你有一个循环执行一定数量的迭代,但你使用计数器变量作为一个索引到一个数量少于那个项目的数组,如为:
int a[50], x;
for( i=0; i < 1000; i++) x = a[i];
编译器可以确定此循环将尝试访问数组外的内存&#39; a&#39;。编译器用这个相当神秘的消息抱怨这个:
迭代xxu调用未定义的行为[-Werror = aggressive-loop-optimizations]