为什么这个循环会产生“警告: 迭代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值被破坏的溢出操作?

我已经阅读了 为什么使用 GCC 的 x86上的整数溢出会导致无限循环?的答案,但我仍然不清楚在 为什么这种情况发生-我得到的“未定义”意味着“任何事情都可能发生”,但是什么是 这种特殊的行为的根本原因?

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

编译器: gcc (4.8)

81572 次浏览

有符号整数溢出(严格来说,没有所谓的“无符号整数溢出”)意味着 不明确的行为。这意味着任何事情都可能发生,讨论为什么会在 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;

@ T. C 的评论:

我怀疑它是这样的: (1)因为每次使用大于2的 i迭代都有未定义行为-> (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...

不幸的是,这就是编写错误代码的后果。

幸运的是,您可以使用更好的诊断和更好的调试工具——这就是它们的用途:

  • 启用所有警告

  • -Wall是 gcc 选项,它支持所有有用的警告,不会出现误报。这是您应该始终使用的最小值。

  • Gcc 有许多其他的警告选项 ,但是 -Wall没有启用这些选项,因为它们可能会对假阳性发出警告

  • 不幸的是,Visual C + + 在提供有用的警告方面落在了后面。至少 IDE 在默认情况下启用了一些。

  • 使用调试标志进行调试

    • 对于整数溢出 -ftrapv陷阱程序的溢出,
    • Clang 编译器在这方面非常出色: -fcatch-undefined-behavior捕获大量未定义行为的实例(注意: "a lot of" != "all of them")

我有一个意大利面条的程序不是我写的,需要明天发货! 救命! ! ! 111one

使用 GCC 的 -fwrapv

这个选项指示编译器假设加减乘除的符号算术溢出使用双补表示。

1 -此规则不适用于“无符号整数溢出”,如3.9.1.4所说

无符号整数,声明为无符号,应遵守算术模2N的规律,其中 n 为数字 在整数的特定大小的值表示中的位。

例如,算术模2N的规则从数学上定义了 UINT_MAX + 1的结果

我不明白的是为什么 i 值被溢出操作破坏了?

似乎整数溢出发生在第4次迭代中(对于 i = 3)。 整数溢出调用 未定义行为。在这种情况下,没有什么是可以预测的。循环可能只迭代 4次,也可能无限循环或其他循环!
结果可能因编译器不同而不同,甚至可能因同一编译器的不同版本而不同。

未定义行为:

本国际标准对其没有要求的行为
[注意: 当这个国际标准遗漏了任何明确的行为定义,或者当程序使用了错误的结构或者错误的数据时,可能会出现未定义行为。允许的未定义行为包括: 完全忽略不可预测的结果,在翻译或程序执行过程中以有记录的环境特征的方式行事(发布或不发布诊断信息) ,终止翻译或执行(发布诊断信息).许多错误的程序结构不会产生未定义行为,它们需要被诊断。 ー尾注]

该代码生成一个测试,整数 + 正整数正整数 = = 负整数。通常优化器不会优化这个测试,但是在接下来使用 std::endl的特定情况下,编译器会优化这个测试。我还没弄明白 endl有什么特别之处。


从 -O1及更高级别的汇编代码可以清楚地看出,gcc 将循环重构为:

i = 0;
do {
cout << i << endl;
i += NUMBER;
}
while (i != NUMBER * 4)

正常工作的最大值是 715827882,即 floor (INT_MAX/3):

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在2中的补数。

命中 -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专门记录了这个问题,我们可以看到在 Gcc 4.8版本说明中说(强调我的未来) :

海湾合作委员会现在使用一个更强的主动分析得出一个上限 循环的迭代次数 语言标准 。这可能导致不一致的程序不符合 正如预期的那样,工作时间更长,例如 SPECCPU 2006464.h264ref 和 增加了一个新的选项-fno-主动-循环优化,以禁用这种主动分析 已知迭代次数不变但未定义行为是已知的 在到达之前或在最后一次迭代期间发生在循环中 将警告循环中的未定义行为,而不是派生 循环的迭代次数的下限 警告可以通过 -Wno-主动-循环-优化来禁用。

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

长的答案开始于知道 有符号整数溢出的未定义行为,通过查看 C + + 标准部分草案 5标准部分 表达方式段落 4说:

如果在计算表达式期间,< strong > 结果不是 数学上定义的或不在可表示值的范围内 它的类型,行为是未定义的 C + + 的实现忽略整数溢出 乘以零,形成一个使用零除数的余数,并且全部浮动 点异常因机器而异,通常由 库函数ーー结束注释

我们知道,标准规定,未定义行为是不可预测的,因为它的定义是:

[注意: 本未定义行为可能会在下列情况出现: 标准省略任何明确的行为定义或程序 使用错误的结构或错误的数据 行为范围从完全无视情况 不可预测的结果 ,在翻译或编程过程中的行为 以环境特有的文件化方式执行 (有或没有发出诊断信息) ,终止 翻译或执行(发布诊断结果) 许多错误的程序结构不会产生未定义的 行为; 他们需要被诊断

但是,gcc优化器究竟能做些什么来把这个问题变成一个无限循环呢?听起来很奇怪。不过谢天谢地,gcc在警告中为我们提供了解决这个问题的线索:

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

线索是 Waggressive-loop-optimizations,那是什么意思?对我们来说幸运的是,这不是第一次这种优化以这种方式破坏代码,我们很幸运,因为 John Regehr在文章 海湾合作委员会前4.8打破破规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;
}

文章说:

未定义行为正在访问 d [16] ,然后退出 在 C99中,创建一个元素的指针是合法的 位置超过数组的末尾,但该指针不能 解除引用。

后来他说:

C 编译器在看到 d [ + + k ]后, 允许假定 k 的增量值在 数组边界,否则会出现未定义行为 在这里,< strong > GCC 可以推断 k 在0. . 15的范围内 海湾合作委员会看到 k < 16,它对自己说: “啊哈——这个表达总是 所以我们有一个无限循环。” 这里的情况是 编译器使用定义良好的假设来推断一个有用的 数据流事实,

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

他解释说,这与臭名昭著的 取消 Linux 内核空指针检查非常相似,在这里可以看到以下代码:

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

gcc推断由于 ss->f;中被延迟,并且由于取消引用一个空指针是未定义行为的,那么 s一定不能是空的,因此优化了下一行的 if (!s)检查。

这里的教训是,现代优化器在利用未定义行为方面非常积极,而且很可能只会变得更加积极。通过几个例子,我们可以清楚地看到优化器所做的事情对于程序员来说似乎完全不合理,但是从优化器的角度来看,这些事情是有意义的。

在 gcc 中报告这个错误的另一个例子是,当您有一个循环执行固定数量的迭代,但是您使用计数器变量作为索引进入一个项数量少于该数量的数组时,例如:

int a[50], x;


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

编译器可以确定这个循环将尝试访问数组‘ a’之外的内存。编译器抱怨说:

迭代 xXu 调用未定义行为[-Werror = 咄咄逼人的循环优化]