为什么这个循环产生“警告:迭代3u调用未定义的行为”并输出超过4行?

编译这个:

#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价值被溢出操作打破了?

我读过的答案为什么整数溢出在x86与GCC导致无限循环? ,但我仍然不清楚为什么发生这种情况 – 我得到的“未定义”是指“任何事情都可能发生”,但是这种特定行为的根本原因是什么?

在线: http : //ideone.com/dMrRKR

编译器: gcc (4.8)

有符号的整数溢出(严格地说,没有“无符号整数溢出”这样的意思)意味着未定义的行为 。 这意味着任何事情都可能发生,并讨论为什么在C ++规则下发生这种事情是没有意义的。

C ++ 11草案N3337:§5.4: 1

如果在expression式评估过程中,结果不是math上定义的,或者不在其types的可表示值范围内,则行为是不确定的。 [注:C ++的大多数现有实现忽略整数溢出。 除零的处理,使用零除数形成余数,所有浮点exception在不同的机器之间变化,通常可以通过库函数进行调整。 – 注意]

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)因为每次有任何大于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; } 

事实上,正确的代码有终止条件。

  ; ...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,这完全不明显! 这不公平! 我要求试火!

处理它,你写了错误的代码,你应该感觉不好。 承担后果。

…或者,也可以正确使用更好的诊断工具和更好的debugging工具 – 这就是他们的目标:

  • 启用所有警告

    • -Wall是gcc选项,使所有有用的警告没有误报。 这是你应该始终使用的最低限度。
    • 海湾合作委员会有许多其他的警告选项 ,但是,他们没有启用-Wall因为他们可能会警告误报
    • 不幸的是,Visual C ++能够提供有用的警告。 至lessIDE启用了一些默认情况下。
  • 使用debugging标志进行debugging

    • 对于整数溢出-ftrapv陷阱溢出程序,
    • Clang编译器对此非常好: -fcatch-undefined-behavior捕获很多未定义行为的实例(注意: "a lot of" != "all of them"

我有一个不是我写的程序的意大利面混乱,明天需要运送! HELP !!!!!! 111oneone

使用gcc的-fwrapv

该选项指示编译器假设使用二进制补码表示法的加法,减法和乘法的有符号算术溢出循环。

1 – 这个规则不适用于“无符号整数溢出”,正如§3.9.1.4所说的那样

无符号整数(无符号整数)应遵循算术模2 n的定律,其中n是该特定整数大小的值表示中的位数。

并且例如UINT_MAX + 1结果在math上被定义 – 通过算术模2 n的规则

简单的回答, gcc专门logging了这个问题,我们可以看到在gcc 4.8发行说明里面( 强调我的前进 ):

GCC现在使用更积极的分析来使用语言标准强加的约束来得出循环迭代次数的上限 。 这可能会导致不符合要求的程序不能按预期工作,如SPEC CPU 2006 464.h264ref和416.gamess。 增加了一个新选项-fno-aggressive-loop-optimizations来禁用这种激进的分析。 在一些已知迭代次数恒定的循环中,但是在迭代之前或者在最后一次迭代期间,循环中出现未定义的行为,GCC会警告循环中未定义的行为,而不是导出迭代次数的较低的上限为循环。 警告可以通过-Wno-aggressive-loop-optimizations来禁用。

实际上,如果我们使用-fno-aggressive-loop-optimizations则无限循环行为应该停止,并且在所有testing的情况下都会停止。

长的答案开始于知道有符号的整数溢出是未定义的行为通过查看草案C ++标准第5expression式4段说:

如果在expression式评估过程中, 结果不是math定义的,或者不在其types的可表示值范围内,则行为是未定义的 。 [注:大多数现有的C ++实现忽略整数溢出。 除零的处理,使用零除数形成余数,所有浮点exception在不同机器之间不同,通常可以通过库函数进行调整。 – 结束注释

我们知道这个标准说明未定义的行为是不可预知的,

[注:当本标准忽略任何明确的行为定义或程序使用错误的结构或错误数据时,可能会出现未定义的行为。 允许的未定义的行为范围从完全忽略情况,以不可预知的结果 ,在翻译或程序执行期间以文档化的方式performance环境特征(不论是否发布诊断消息),终止翻译或执行(发行的诊断消息)。 许多错误的程序结构不会产生未定义的行为; 他们需要被诊断。 – 注意]

但是gcc优化者可以在这个世界上做些什么来把它变成一个无限循环? 这听起来完全古怪。 但幸好gcc给了我们一个线索,在警告中搞清楚:

 warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations] std::cout << i*1000000000 << std::endl; ^ 

这个线索就是Waggressive-loop-optimizations ,这是什么意思? 幸运的是,对于我们来说,这不是第一次以这种方式优化代码,我们很幸运,因为John Regehr已经在文章GCC 4.8之前的文章中logging了一个案例,它显示了以下代码:

 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中,创build一个指向元素的指针是合法的,但是该指针不能被解除引用。

后来又说:

详细地说,这是发生了什么事情。 AC编译器在看到d [++ k]时,被允许假定增加的k值在数组边界内,否则就会发生未定义的行为。 对于这里的代码, GCC可以推断k在0..15范围内。 稍后,当GCC看到k <16时,它自己说:“啊哈 – expression总是正确的,所以我们有一个无限循环。”这里的情况,编译器使用假设定义好推断有用的数据stream事实上,

所以在某些情况下编译器必须做的是假设,因为有符号整数溢出是未定义的行为,那么i必须总是小于4 ,因此我们有一个无限循环。

他解释说,这是非常相似的臭名昭着的Linux内核空指针检查删除在哪里看到这个代码:

 struct foo *s = ...; int x = s->f; if (!s) return ERROR; 

gcc推断,因为s是推定在s s->f; 并且由于解引用空指针是未定义的行为,所以s不能为空,因此优化了下一行的if (!s)检查。

这里的教训是,现代优化器非常积极地利用未定义的行为,而且很可能只会变得更加激进。 显然,只用几个例子就可以看出优化器对程序员来说看起来完全不合理,但从优化器的angular度来看,这是合理的。

tl; dr该代码生成一个整数 + 正整数 == 负整数的testing 。 通常情况下,优化器不会优化这个,但是在接下来使用std::endl的特定情况下,编译器会优化这个testing。 我还没有弄清楚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 

注意, -14316557684 * 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重新编译,则715827883版本与改变的编号和testing值相同。 然而,-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, %esicmpl $-1431655764, %esi ,该行已被删除-O2 。 优化器必须已经决定将715827883添加到%esi永远不能等于-1431655764

这很令人费解。 将其添加到INT_MIN+1 的确会生成预期结果,所以优化器必须已经决定%esi永远不能是INT_MIN+1 ,我不确定为什么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);replace它std::cout.put('\n'); std::flush(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汇编)。

我无法得到的是为什么我的价值被溢出操作打破了?

看来,整数溢出发生在第四次迭代(对于i = 3 )。 signed整数溢出调用未定义的行为 。 在这种情况下,什么都不能预测。 循环可能只迭代4次,或者它可能会无限或其他任何东西!
结果可能会改变编译器到编译器,甚至不同编译器的不同版本。

C11:1.3.24未定义的行为:

本国际标准没有规定的行为
[注:当本标准忽略任何明确的行为定义或程序使用错误的结构或错误数据时,可能会出现未定义的行为。 允许的未定义的行为范围从完全忽略情况,以不可预知的结果,在翻译或程序执行期间以文档化的方式performance环境特征(不论是否发布诊断消息),终止翻译或执行(发行的诊断消息) 。 许多错误的程序结构不会产生未定义的行为; 他们需要被诊断。 – 注意]

在gcc中报告的这个错误的另一个例子是当你有一个循环执行一个不断迭代次数的循环,但是你使用计数器variables作为一个数组less于这个数目的数组的索引,例如:

 int a[50], x; for( i=0; i < 1000; i++) x = a[i]; 

编译器可以确定这个循环将尝试访问数组“a”之外的内存。 编译器抱怨这个相当神秘的消息:

迭代xxu调用未定义的行为[-Werror = aggressive-loop-optimizations]