zer*_*kms 160 c++ gcc undefined-behavior
编译:
#include <iostream>
int main()
{
for (int i = 0; i < 4; ++i)
std::cout << i*1000000000 << std::endl;
}
Run Code Online (Sandbox Code Playgroud)
并gcc产生以下警告:
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
Run Code Online (Sandbox Code Playgroud)
我知道有一个有符号的整数溢出.
我不能得到的是为什么i价值被溢出操作打破了?
我已经阅读了为什么x86上的整数溢出与GCC导致无限循环的答案?,但我仍然不清楚为什么会发生这种情况 - 我认为"未定义"意味着"任何事情都可能发生",但这种特定行为的根本原因是什么?
编译: gcc (4.8)
mil*_*bug 106
有符号整数溢出(严格来说,没有"无符号整数溢出"这样的东西)意味着未定义的行为.这意味着任何事情都可能发生,并讨论为什么它会在C++规则下发生没有意义.
C++ 11草案N3337:§5.4:1
如果在评估表达式期间,结果不是在数学上定义的,或者在其类型的可表示值范围内没有,则行为未定义.[注意:大多数现有的C++实现忽略整数流.除零处理,使用零除数形成余数,所有浮点异常因机器而异,通常可通过库函数调整. - 尾注]
您的代码编译时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)
^
Run Code Online (Sandbox Code Playgroud)
我们分析程序正在做什么的唯一方法是读取生成的汇编代码.
这是完整的装配清单:
.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
Run Code Online (Sandbox Code Playgroud)
我甚至几乎不能阅读装配,但即使我能看到这addl $1000000000, %edi条线.结果代码看起来更像
for(int i = 0; /* nothing, that is - infinite loop */; i += 1000000000)
std::cout << i << std::endl;
Run Code Online (Sandbox Code Playgroud)
@TC的评论:
我怀疑它是这样的:(1)因为
i任何大于2的值的迭代都有未定义的行为 - >(2)我们可以假设i <= 2为了优化目的 - >(3)循环条件总是为真 - >(4) )它被优化成无限循环.
让我想到将OP代码的汇编代码与下面代码的汇编代码进行比较,没有未定义的行为.
#include <iostream>
int main()
{
// changed the termination condition
for (int i = 0; i < 3; ++i)
std::cout << i*1000000000 << std::endl;
}
Run Code Online (Sandbox Code Playgroud)
事实上,正确的代码具有终止条件.
; ...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...
Run Code Online (Sandbox Code Playgroud)
天哪,这完全不明显!这不公平!我要求用火试验!
处理它,你写了错误的代码,你应该感觉很糟糕.承担后果.
......或者,正确使用更好的诊断和更好的调试工具 - 这就是它们的用途:
启用所有警告
-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数学定义的结果- 由算术模2n的规则
Sha*_*our 68
简短的回答,gcc特别记录了这个问题,我们可以看到gcc 4.8发布说明中的内容(强调我的前进):
GCC现在使用更积极的分析来使用语言标准强加的约束来导出循环迭代次数的上限.这可能导致不合规程序不再按预期工作,例如SPEC CPU 2006 464.h264ref和416.gamess.添加了一个新选项-fno-aggressive-loop-optimizations以禁用此积极分析.在一些具有已知常数迭代次数的循环中,已知未定义的行为在到达或在最后一次迭代期间在循环中发生,GCC将警告循环中的未定义行为,而不是导出迭代次数的下限上限为循环.可以使用-Wno-aggressive-loop-optimizations禁用警告.
事实上,如果我们使用-fno-aggressive-loop-optimizations无限循环行为应该停止,它在我测试的所有情况下都会停止.
通过查看草案C++标准部分表达式第4段,知道有符号整数溢出是未定义的行为,这个答案很长,其中说:5
如果在评估表达式期间,结果未在数学上定义或未在其类型的可表示值范围内,则行为未定义.[注意:大多数现有的C++实现忽略整数溢出.除零处理,使用零除数形成余数,所有浮点异常因机器而异,通常可通过库函数调整. - 注意
我们知道该标准表明未定义的行为是不可预测的,因为该定义附带的说明如下:
[注意:当本国际标准忽略任何明确的行为定义或程序使用错误的构造或错误数据时,可能会出现未定义的行为.允许的未定义行为包括完全忽略不可预测的结果,在翻译或程序执行期间以环境特征(有或没有发出诊断消息)的特定行为,终止翻译或执行(发布时)一条诊断信息).许多错误的程序结构不会产生未定义的行为; 他们需要被诊断出来. - 尾注]
但是,gcc优化器可以做些什么来将其转变为无限循环?听起来很古怪.但幸运的是gcc,我们在警告中找到了解决它的线索:
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
Run Code Online (Sandbox Code Playgroud)
线索是Waggressive-loop-optimizations,这是什么意思?幸运的是,对于我们来说,这并不是第一次这种优化以这种方式破坏代码而且我们很幸运,因为John Regehr已经在GCC 4.8版Breaks Broken SPEC 2006基准测试中记录了一个案例,其中显示了以下代码:
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;
}
Run Code Online (Sandbox Code Playgroud)
文章说:
未定义的行为是在退出循环之前访问d [16].在C99中,创建指向一个位置超过数组末尾的元素的指针是合法的,但不能取消引用该指针.
后来说:
详细而言,这是正在发生的事情.在看到d [++ k]时,AC编译器被允许假设k的递增值在数组边界内,因为否则会发生未定义的行为.对于此处的代码,GCC可以推断出k在0..15的范围内.稍后,当GCC看到k <16时,它对自己说:"啊哈 - 那个表达式总是正确的,所以我们有一个无限循环."这里的情况,编译器使用明确定义的假设来推断a有用的数据流事实,
那么编译器在某些情况下必须做的是假设因为有符号整数溢出是未定义的行为然后i必须总是小于4,因此我们有一个无限循环.
他解释说这非常类似于臭名昭着的Linux内核空指针检查删除,在这里看到这段代码:
struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;
Run Code Online (Sandbox Code Playgroud)
gcc推断,由于s在被deferenced s->f;,自提领一空指针是未定义的行为则s不能为空,因此优化走if (!s)下一行检查.
这里的教训是,现代优化器在利用未定义的行为方面非常积极,而且很可能只会变得更具侵略性.显然,只有几个例子,我们可以看到优化器对程序员做了一些看起来完全不合理的事情,但从优化器的角度回顾是有道理的.
M.M*_*M.M 23
tl; dr代码生成一个整数 + 正整数 == 负整数的测试.通常,优化器不会对此进行优化,但std::endl在下一个使用的特定情况下,编译器会优化此测试.我还没弄清楚到底有什么特别之处endl.
从-O1和更高级别的汇编代码,很明显gcc将循环重构为:
i = 0;
do {
cout << i << endl;
i += NUMBER;
}
while (i != NUMBER * 4)
Run Code Online (Sandbox Code Playgroud)
正确工作的最大值是715827882,floor(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
Run Code Online (Sandbox Code Playgroud)
注意,-1431655768是4 * 7158278822的补码.
点击-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
Run Code Online (Sandbox Code Playgroud)
因此,所做的优化仅仅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
Run Code Online (Sandbox Code Playgroud)
凡有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);
}
}
Run Code Online (Sandbox Code Playgroud)
如果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
Run Code Online (Sandbox Code Playgroud)
用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
Run Code Online (Sandbox Code Playgroud)
这两者之间的区别在于%esi原始%ebx版本和第二版本中使用的区别; 在一般情况下%esi和%ebx一般情况下定义的语义是否存在差异?(我对x86汇编不太了解).
小智 7
在gcc中报告此错误的另一个示例是当您有一个循环执行一定数量的迭代时,但是您使用计数器变量作为具有少于该项数的数组的索引,例如:
int a[50], x;
for( i=0; i < 1000; i++) x = a[i];
Run Code Online (Sandbox Code Playgroud)
编译器可以确定此循环将尝试访问数组'a'之外的内存.编译器用这个相当神秘的消息抱怨这个:
迭代xxu调用未定义的行为[-Werror = aggressive-loop-optimizations]
我不能得到的是为什么我的值被溢出操作打破了?
似乎整数溢出发生在第4次迭代(for i = 3)中.
signed整数溢出调用未定义的行为.在这种情况下,无法预测任何事情.循环可能只迭代4次数,或者它可能会变为无限或其他任何东西!
结果可能会因编译器与编译器的不同而不同,甚至可能因同一编译器
本国际标准没有要求的
行为[注意:当本国际标准忽略任何明确的行为定义或程序使用错误的结构或错误数据时,可能会出现未定义的行为.允许的未定义行为包括完全忽略不可预测的结果,在翻译或程序执行期间以环境特征(有或没有发出诊断消息)的特定行为,终止翻译或执行(发布时)一条诊断信息).许多错误的程序结构不会产生未定义的行为; 他们需要被诊断出来. - 尾注]