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

时间:2014-06-18 23:27:31

标签: c++ gcc undefined-behavior

编译:

#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?的答案,但我仍然不清楚为什么这种情况发生了 - 我明白了#34;未定义&#34;意味着&#34;任何事情都可能发生&#34;,但这种特定行为的根本原因是什么?

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

编译器:gcc (4.8)

5 个答案:

答案 0 :(得分:103)

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

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

  

如果在评估表达式期间,结果在数学上没有定义或在范围内没有   其类型的可表示值,行为未定义。 [注意:大多数现有的C ++实现   忽略整数而不是flows。除以零的处理,使用零除数形成余数,以及所有   浮动点异常因机器而异,通常可通过库函数进行调整。 - 后注]

使用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的每次迭代都有未定义的行为 - &gt; (2)我们可以假设i <= 2用于优化目的 - &gt; (3)循环条件总是正确的 - &gt; (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,这完全不明显!这不公平!我要求用火试验!

处理它,你编写了错误的代码,你应该感觉很糟糕。承担后果。

......或者,正确使用更好的诊断和更好的调试工具 - 这就是它们的用途:

  • 启用所有警告

    • -Wall是gcc选项,可以启用所有有用的警告而不会出现误报。这是您应该经常使用的最低限度。
    • gcc has many other warning options但是,他们未启用-Wall,因为他们可能会对误报发出警告
    • 不幸的是,Visual C ++在提供有用警告的能力方面落后了。至少IDE默认启用一些。
  • 使用调试标志进行调试

    • for integer overflow -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的结果在数学上被定义 - 由算术模2的规则 n

答案 1 :(得分:64)

简短回答,gcc专门记录了这个问题,我们可以在gcc 4.8 release notes中看到(强调我的前进):

  

GCC现在使用更强大的积极分析来获得上限   使用约束强加的循环迭代次数   语言标准。这可能导致不合格的程序为否   按预期工作的时间更长,例如SPEC CPU 2006 464.h264ref和   416.gamess。添加了一个新选项-fno-aggressive-loop-optimizations以禁用此积极分析。在一些循环中   已知的常数迭代次数,但已知未定义的行为   在到达或在最后一次迭代期间,在GCC中发生   将警告循环中的未定义行为而不是派生   循环迭代次数的下限。该   可以使用-Wno-aggressive-loop-optimizations禁用警告。

而且如果我们使用-fno-aggressive-loop-optimizations,无限循环行为应该停止,并且在我测试的所有情况下都会这样做。

通过查看草案C ++标准部分5 表达式 4 <,知道有符号整数溢出是未定义的行为,这个答案很长/ em>其中说:

  

如果在评估表达式时,结果不是   在数学上定义或不在可表示值的范围内   它的类型,行为未定义。 [注:大多数现有   C ++的实现忽略整数溢出。治疗师   零,使用零除数形成余数,并且全部浮动   点异常因机器而异,通常可通过a调整   库函数。 - 注意

我们知道该标准表示未定义的行为是不可预测的,因为该定义附带的说明如下:

  

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

但是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中记录了一个案例,其中显示了以下代码: / p>

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中,创建指向元素1的指针是合法的   位于数组末尾的位置,但该指针不得为   解除引用。

后来说:

  

详细说明,这是正在发生的事情。一个C编译器,看到d [++ k],   允许假设k的增量值在   数组边界,因为否则会发生未定义的行为。对于代码   在这里, GCC可以推断出k在0..15的范围内。稍后,什么时候   海湾合作委员会认为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) 1}}检查下一行。

这里的教训是,现代优化器在利用未定义的行为方面非常积极,而且很可能只会变得更具侵略性。显然,只有几个例子,我们可以看到优化器对程序员做了一些看起来完全不合理的事情,但从优化器的角度回顾是有道理的。

答案 2 :(得分:23)

tl; dr 代码生成的测试整数 + 正整数 == 负整数。通常,优化器不会对此进行优化,但在接下来使用std::endl的特定情况下,编译器会优化此测试。我还没弄清楚endl还有什么特别之处。


从-O1及更高级别的汇编代码中,很明显gcc将循环重构为:

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

正确运行的最大值是715827882,即楼层(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

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汇编不太了解。)

答案 3 :(得分:6)

  

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

似乎整数溢出发生在第4次迭代中(对于i = 3)。 signed整数溢出调用未定义的行为。在这种情况下,无法预测任何事情。循环可能只迭代4次,或者它可能会变为无限或其他任何东西! 结果可能会因编译器与编译器的不同而不同,甚至可能因同一编译器

C11:1.3.24未定义的行为:

  

本国际标准没有要求的行为
  [注意:当本国际标准忽略任何明确的行为定义或程序使用错误的构造或错误数据时,可能会出现未定义的行为。 允许的未定义行为包括完全忽略不可预测的结果,在转换或程序执行期间以环境特征(有或没有发出诊断消息)的文档方式执行,终止翻译或执行(发布诊断信息。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。    - 后注]

答案 4 :(得分:6)

在gcc中报告此错误的另一个例子是当你有一个循环执行一定数量的迭代,但你使用计数器变量作为一个索引到一个数量少于那个项目的数组,如为:

int a[50], x;

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

编译器可以确定此循环将尝试访问数组外的内存&#39; a&#39;。编译器用这个相当神秘的消息抱怨这个:

  

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