为什么此循环会产生“警告:迭代3u调用未定义的行为”并输出多于4行?


162

编译此:

#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)


49
有符号整数溢出=>未定义行为=>鼻守护程序。但是我不得不承认,这个例子很好。
dyp

1
汇编输出:goo.gl/TtPmZn
Bryan Chen

1
发生在GCC 4.8与O2,和O3标志,而不是O0O1
亚历

3
@dyp,当我阅读Nasal Daemons时,我做了“ imgur大笑”,其中包括当您看到有趣的东西时会微微呼气。然后我意识到...我必须被一个鼻道守护者诅咒!
corsiKa 2014年

4
这添加书签,所以我可以链接它下一次有人反驳“它在技术上UB,但它应该做的事 ” :)
MM

Answers:


107

有符号整数溢出(严格来说,没有“无符号整数溢出”之类的东西)表示未定义的行为。这意味着任何事情都可能发生,并且讨论为什么在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在溢出时捕获程序,
    • 锵编译器非常适合这样的:-fcatch-undefined-behavior抓住了很多不确定的行为实例(注:"a lot of" != "all of them"

我有一个不是由我编写的程序的意大利面烂摊子,明天需要发货!帮助!!!!!! 111oneone

使用gcc -fwrapv

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

1-该规则不适用于“无符号整数溢出”,如第3.9.1.4节所述:

声明为无符号的无符号整数应服从2 n的算术定律,其中n是该特定整数大小的值表示形式中的位数。

并且例如UINT_MAX + 1通过数学模2 n的数学规则来定义


7
我还是不太明白这里发生了什么...为什么i自己会受到影响?通常,未定义的行为没有这些奇怪的副作用,毕竟i*100000000应该是一种右值
vsoftco 2014年

26
我怀疑它是这样的:(1)因为i任何大于2的值的迭代 都具有未定义的行为->(2)我们可以假设i <= 2出于优化目的->(3)循环条件始终为true->(4 )进行了优化,变成了无限循环。
TC

28
@vsoftco:发生了一种情况,就是强度降低,更具体地说是感应变量消除。编译器通过发出代码来消除乘法,该代码将i每次迭代增加1e9(并相应地更改循环条件)。在“好像”规则下,这是一个完全有效的优化,因为该程序在行为良好的情况下无法观察到差异。las,不是,优化“泄漏”了。
JohannesD

8
@JohannesD指出了这种中断的原因。但是,由于循环终止条件不涉及溢出,因此这是一个糟糕的优化。使用强度降低是可以的-我不知道处理器中的乘数会与(4 * 100000000)做什么,而与(100000000 + 100000000 + 100000000 + 100000000)不同,然后回退到“它是未定义的-谁知道“是合理的。但是用执行超过4次的东西替换“应该是行为良好的循环”,因为它执行4次并产生未定义的结果。是白痴。
朱莉在奥斯丁

14
@JulieinAustin虽然这对您来说可能是愚蠢的,但这是完全合法的。从积极的方面来说,编译器会警告您。
milleniumbug

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;
                  ^

线索是Waggressive-loop-optimizations,这是什么意思?对我们来说幸运的是,这不是这种优化方法第一次不是这样破坏代码的,而我们很幸运,因为John RegehrGCC 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中,创建指向数组末尾一个位置的元素的指针是合法的,但是不得取消对该指针的引用。

然后说:

详细地,这是怎么回事。看到d [++ k]时,允许AC编译器假定k的递增值在数组范围内,因为否则会发生未定义的行为。对于此处的代码,GCC可以推断出k在0..15范围内。稍后,当GCC看到k <16时,它自言自语:“啊哈-这个表达式总是正确的,所以我们有一个无限循环。” 在这种情况下,编译器使用定义明确的假设来推断有用的数据流事实,

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

他解释说,这与臭名昭著的Linux内核null指针检查删除非常相似,在该代码中看到了以下代码:

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

gcc推断自从ss->f;引用以来,由于取消引用空指针是未定义的行为s,因此不能为空,因此可以优化if (!s)下一行的检查。

这里的教训是,现代优化器对于利用未定义的行为非常激进,并且很有可能只会变得更加激进。显然,仅需几个示例,我们就可以看到优化器所做的事情对于程序员来说似乎是完全不合理的,但是从优化器的角度进行回顾是有意义的。


7
我了解这就是编译器编写者正在做的事情(我曾经编写过编译器,甚至是两个优化器),但是即使它们是“未定义的”,也存在一些“有用的”行为,并且这种行为正朝着更加积极的优化迈进只是精神错乱。您上面引用的构造是错误的,但是优化错误检查是对用户不利的。
朱莉在奥斯丁

1
@JulieinAustin我同意这是非常令人惊讶的行为,他说开发人员需要避免未定义的行为实际上只是问题的一半。显然,编译器还需要向开发人员提供更好的反馈。在这种情况下,会产生警告,尽管它实际上并不能提供足够的信息。
Shafik Yaghmour 2014年

3
我认为这是一件好事,我想要更好,更快的代码。UB永远不要使用。
paulm 2014年

1
@paulm从道德上讲,UB显然很糟糕,但是很难提供更好的工具,例如clang静态分析器,以帮助开发人员在影响生产应用程序之前捕获UB和其他问题。
Shafik Yaghmour 2014年

1
@ShafikYaghmour另外,如果您的开发人员忽略警告,他们将有多少机会关注clang输出?积极的“无正当警告”政策很容易抓住这个问题。建议使用Clang,但不是必需的。
deworde 2014年

24

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

注意,-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

那里的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汇编了解不多)。


最好确切地了解优化器的逻辑是什么,在这个阶段对我来说尚不清楚,为什么某些情况下对测试进行了优化而
MM

8

在gcc中报告此错误的另一个示例是,当您执行一个循环以执行恒定数量的迭代,但是您将计数器变量用作具有少于该数目的项的数组的索引时,例如:

int a[50], x;

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

编译器可以确定此循环将尝试访问数组“ a”之外的内存。编译器通过以下相当隐秘的消息抱怨这一点:

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


更隐秘的是,仅在优化打开时才发出消息。M $ VB消息“数组超出范围”是给傻瓜使用的吗?
拉维·加内什

6

我无法获得的是为什么我的价值被该溢出操作所破坏?

似乎在(i = 3)的第4次迭代中发生了整数溢出。 signed整数溢出会调用未定义的行为。在这种情况下,无法预测。该循环可能仅迭代4几次,也可能进入无限循环或其他任何循环!
结果可能因编译器而异,甚至对于同一编译器的不同版本也可能不同。

C11:1.3.24未定义的行为:

本国际标准不施加任何要求的
行为[注:当本国际标准省略行为的任何明确定义或程序使用错误的构造或错误的数据时,可能会出现未定义的行为。允许的不确定行为包括从完全忽略情况以无法预测的结果到在翻译或程序执行过程中以环境特征的书面形式记录的行为(带有或不带有诊断消息)到终止翻译或执行(带有发布)诊断消息)。许多错误的程序构造不会引起未定义的行为。他们需要被诊断。—尾注]


@bits_international; 是。
鹰头鹰嘴

4
您说的没错,可以公平地解释我为什么投票。这个答案中的信息是正确的,但它不是教育性的信息,它完全忽略了房间里的大象:破损显然发生与造成溢出的操作不同的地方(停止条件)。尽管这是此问题的核心,但并未说明在这种特定情况下如何破坏事物的机制。这是典型的教师状况不佳的情况,教师的回答不仅不能解决问题的核心,而且不鼓励其他问题。几乎听起来像是……
Szabolcs

5
“我看到这是不确定的行为,从现在开始,我不在乎它如何或为什么破坏。标准允许它破坏。没有其他问题。” 您可能并没有那样的意思,但似乎是那样。我希望对SO的这种(不幸的是普遍的)态度较少。这实际上没有用。如果您得到用户输入,则即使标准说明程序的任何其他部分都可能因此而崩溃,在每次单个有符号整数运算之后检查溢出是否合理。了解如何它打破确实在实践中这样的帮助,避免出现问题。
Szabolcs

2
@Szabolcs:最好将C视为两种语言,其中一种旨在使简单的编译器在程序员的帮助下,利用能够在其预期目标平台上可靠的结构来实现合理有效的可执行代码。其他语言,因此被标准委员会忽略,第二种语言排除了标准不要求其支持的所有此类构造,目的是允许编译器应用其他优化,这些优化可能会或可能不会超过程序员所必需的。放弃。
超级猫

1
@Szabolcs“ 如果得到用户输入,则在每次单个有符号整数运算之后检查溢出是否合理 ”-正确,因为在那一点上来不及了。您必须每个单个有符号整数运算之前检查溢出。
melpomene
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.