编译这个:
#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? 的答案,但我仍然不清楚为什么会发生这种情况 - 我知道“未定义”的意思是“任何事情都可能发生”,但 的根本原因是什么em>这个特定的行为?
编译器:gcc (4.8)
O2
和 O3
标志的 GCC 4.8 上发生,但不是 O0
或 O1
有符号整数溢出(严格来说,没有“无符号整数溢出”之类的东西)意味着未定义的行为。这意味着任何事情都可能发生,在 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)
^
我们可以分析程序在做什么的唯一方法是阅读生成的汇编代码。
这是完整的组装清单:
.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;
@TC 的评论:
我怀疑它是这样的:(1)因为 i 的任何大于 2 的值的每次迭代都有未定义的行为 ->(2)我们可以假设 i <= 2 用于优化目的 ->(3)循环条件总是true -> (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...
不幸的是,这是编写错误代码的后果。
幸运的是,您可以使用更好的诊断和调试工具 - 这就是它们的用途:
启用所有警告
-Wall 是 gcc 选项,它启用所有有用的警告,没有误报。这是您应该始终使用的最低要求。
gcc 有许多其他警告选项,但是,它们没有使用 -Wall 启用,因为它们可能会在误报时发出警告
不幸的是,Visual C++ 在提供有用警告的能力方面落后了。至少 IDE 默认启用一些。
使用调试标志来调试整数溢出 -ftrapv 在溢出时捕获程序,Clang 编译器非常适合: -fcatch-undefined-behavior 捕获许多未定义行为的实例(注意:“很多”!=“所有他们”)
对于整数溢出 -ftrapv 在溢出时捕获程序,
Clang 编译器非常适合:-fcatch-undefined-behavior 捕获很多未定义行为的实例(注意:“很多”!=“所有这些”)
我有一个不是我写的程序的意大利面,需要明天发货!帮助!!!!!!111oneone
使用 gcc 的 -fwrapv
此选项指示编译器假设加法、减法和乘法的有符号算术溢出使用二进制补码表示来回绕。
- 此规则不适用于“无符号整数溢出”,如 §3.9.1.4 所述
声明为无符号的无符号整数应遵守算术模 2n 的定律,其中 n 是该特定整数大小的值表示中的位数。
例如 UINT_MAX + 1
的结果在数学上定义 - 由算术模 2n 的规则
简短的回答,gcc
专门记录了这个问题,我们可以在 gcc 4.8 release notes 中看到(emphasis mine going forward):
GCC 现在使用更积极的分析来使用语言标准施加的约束来推导循环迭代次数的上限。这可能会导致不符合要求的程序不再按预期工作,例如 SPEC CPU 2006 464.h264ref 和 416.gamess。添加了一个新选项 -fno-aggressive-loop-optimizations 以禁用这种激进分析。在一些已知迭代次数为常数的循环中,但在到达之前或最后一次迭代期间已知循环中会发生未定义行为,GCC 将警告循环中未定义的行为,而不是推导迭代次数的下限为循环。可以使用 -Wno-aggressive-loop-optimizations 禁用警告。
事实上,如果我们使用 -fno-aggressive-loop-optimizations
,无限循环行为应该会停止,并且在我测试过的所有情况下都会停止。
长答案开始于通过查看草案 C++ 标准部分 5
Expressions 段落 4 来了解 signed integer 溢出是未定义的行为:
如果在计算表达式期间,结果未在数学上定义或不在其类型的可表示值范围内,则行为未定义。 [注意:大多数现有的 C++ 实现忽略整数溢出。除以零的处理,使用零除数形成余数,所有浮点异常因机器而异,通常可以通过库函数进行调整。 ——尾注
我们知道标准说未定义的行为是不可预测的,从定义附带的注释中可以看出:
[注:当本国际标准省略任何明确的行为定义或程序使用错误的构造或错误数据时,可能会出现未定义的行为。允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(发出的诊断消息)。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。 ——尾注]
但是,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 中记录了一个案例,该案例显示了以下代码:
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 中,创建指向数组末尾后一个位置的元素的指针是合法的,但不能取消引用该指针。
后来说:
详细地说,这是发生了什么。 AC 编译器在看到 d[++k] 时被允许假设 k 的增量值在数组边界内,否则会发生未定义的行为。对于这里的代码,GCC 可以推断出 k 在 0..15 范围内。稍后,当 GCC 看到 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)
检查。
这里的教训是,现代优化器非常积极地利用未定义的行为,而且很可能只会变得更加积极。显然,仅通过几个示例,我们就可以看到优化器所做的事情对程序员来说似乎完全不合理,但从优化器的角度回想起来却是有道理的。
tl;dr 代码生成一个 整数 + 正整数 == 负整数的测试。通常优化器不会对此进行优化,但在接下来使用 std::endl
的特定情况下,编译器会优化此测试。我还没有弄清楚 endl
有什么特别之处。
从 -O1 和更高级别的汇编代码中,很明显 gcc 将循环重构为:
i = 0;
do {
cout << i << endl;
i += NUMBER;
}
while (i != NUMBER * 4)
正确工作的最大值是 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
请注意,-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
在 -O1
有 cmpl $-1431655764, %esi
的地方,已经为 -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
使用 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 汇编不太了解)。
在 gcc 中报告此错误的另一个示例是,当您有一个执行恒定迭代次数的循环时,但您使用 counter 变量作为数组的索引,该数组的项数少于该数,例如:
int a[50], x;
for( i=0; i < 1000; i++) x = a[i];
编译器可以确定此循环将尝试访问数组“a”之外的内存。编译器用这个相当神秘的消息抱怨这个:
迭代 xxu 调用未定义的行为 [-Werror=aggressive-loop-optimizations]
我无法得到的是为什么我的价值被溢出操作破坏了?
似乎整数溢出发生在第 4 次迭代中(对于 i = 3
)。 signed
整数溢出调用未定义的行为。在这种情况下,什么都无法预测。循环可能只迭代 4
次,或者它可能会进入无限期或其他任何情况!
结果可能会因编译器而异,甚至可能因同一编译器的不同版本而异。
C11:1.3.24 未定义的行为:
本国际标准未强加要求的行为 [注:当本国际标准省略任何明确的行为定义或程序使用错误构造或错误数据时,可能会出现未定义的行为。允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(发出的诊断消息)。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。 ——尾注]
i
本身会受到影响?一般来说,未定义的行为不会产生这种奇怪的副作用,毕竟i*100000000
应该是一个右值i
的每次迭代都有未定义的行为 -> (2) 我们可以假设i <= 2
用于优化目的 -> (3) 循环条件始终为真 -> (4) 它被优化成一个无限循环。i
递增 1e9(并相应地更改循环条件)。在“好像”规则下,这是一个完全有效的优化,因为如果程序表现良好,则该程序无法观察到差异。唉,它不是,优化“泄漏”。