繁体   English   中英

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

[英]Why does this loop produce “warning: iteration 3u invokes undefined behavior” and output more than 4 lines?

编译这个:

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

有符号整数溢出(严格来说,没有“无符号整数溢出”这样的东西)意味着未定义的行为 这意味着任何事情都可能发生,讨论为什么会在 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捕获了很多未定义行为的实例(注意: "a lot of" != "all of them"

我有一个不是我写的程序,需要明天发货! 帮助!!!!!!111oneone

使用 gcc 的-fwrapv

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

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

声明为无符号的无符号整数应遵守算术模 2 n的法则,其中 n 是该特定整数大小的值表示中的位数。

并且例如UINT_MAX + 1结果是数学定义的 - 由算术模 2 n的规则

简短的回答, 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++ 标准草案第5表达式4段,知道有符号整数溢出是未定义的行为,答案很长,它说:

如果在对表达式求值期间,结果未在数学上定义或不在其类型的可表示值范围内,则行为为 undefined [ 注意:大多数现有的 C++ 实现都忽略整数溢出。 除以零的处理,使用零除数形成余数,以及所有浮点异常在机器之间有所不同,通常可以通过库函数进行调整。 ——尾注

我们知道标准说未定义的行为是不可预测的,根据定义附带的注释说:

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

但是gcc优化器到底能做什么来把它变成一个无限循环呢? 这听起来很古怪。 但幸运的是gcc在警告中给了我们一个线索来解决它:

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

线索是Waggressive-loop-optimizations ,这是什么意思? 对我们来说幸运的是,这不是这种优化第一次以这种方式破坏代码,我们很幸运,因为John RegehrGCC 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 内核空指针检查删除非常相似,其中看到以下代码:

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

gcc推断,因为ss->f;被尊重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重新编译,那么除了更改的数字和测试值之外, 715827883版本是相同的。 但是,-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::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

使用std::endl-O2 mymanual 内联:

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 未定义行为:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM