ChatGPT解决这个技术问题 Extra ChatGPT

为什么这个循环会产生“警告:迭代 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 值?

我已阅读 Why does integer overflow on x86 with GCC cause an infinite loop? 的答案,但我仍然不清楚为什么会发生这种情况 - 我知道“未定义”的意思是“任何事情都可能发生”,但 的根本原因是什么em>这个特定的行为

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

编译器:gcc (4.8)

有符号整数溢出 => 未定义行为 => 鼻守护程序。但我不得不承认,这个例子非常好。
程序集输出:goo.gl/TtPmZn
在带有 O2O3 标志的 GCC 4.8 上发生,但不是 O0O1
@dyp 当我阅读 Nasal Daemons 时,我做了“imgur 笑”,其中包括当你看到有趣的东西时轻轻地呼气。然后我意识到……我一定是被鼻魔诅咒了!
将此添加书签,以便下次有人反驳“它在技术上是 UB,但它应该做的事情”时可以链接它:)

F
Flexo

有符号整数溢出(严格来说,没有“无符号整数溢出”之类的东西)意味着未定义的行为。这意味着任何事情都可能发生,在 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 捕获许多未定义行为的实例(注意:“很多”!=“所有他们”)

对于整数溢出 -ftrapv 在溢出时捕获程序,

Clang 编译器非常适合:-fcatch-undefined-behavior 捕获很多未定义行为的实例(注意:“很多”!=“所有这些”)

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

使用 gcc 的 -fwrapv

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

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

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

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


我还是不太明白这里发生了什么...为什么 i 本身会受到影响?一般来说,未定义的行为不会产生这种奇怪的副作用,毕竟 i*100000000 应该是一个右值
我怀疑它类似于:(1)因为具有大于 2 的任何值的 i 的每次迭代都有未定义的行为 -> (2) 我们可以假设 i <= 2 用于优化目的 -> (3) 循环条件始终为真 -> (4) 它被优化成一个无限循环。
@vsoftco:发生的是 strength reduction 的情况,更具体地说是 induction variable elimination。编译器通过发出代码来消除乘法,而是在每次迭代时将 i 递增 1e9(并相应地更改循环条件)。在“好像”规则下,这是一个完全有效的优化,因为如果程序表现良好,则该程序无法观察到差异。唉,它不是,优化“泄漏”。
@JohannesD 指出了这种中断的原因。然而,这是一个糟糕的优化,因为循环终止条件不涉及溢出。使用强度降低是可以的——我不知道处理器中的乘数会对 (4*100000000) 做什么,这与 (100000000+100000000+100000000+100000000) 不同,然后退回到“它是未定义的” ——谁知道”是合理的。但是用执行超过 4 次的“因为它是未定义的!”替换应该是一个行为良好的循环,它执行 4 次并产生未定义的结果。是白痴。
@JulieinAustin 虽然这对您来说可能很愚蠢,但它是完全合法的。从积极的方面来说,编译器会警告你。
S
Shafik Yaghmour

简短的回答,gcc 专门记录了这个问题,我们可以在 gcc 4.8 release notes 中看到(emphasis mine going forward):

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

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

长答案开始于通过查看草案 C++ 标准部分 5 Expressions 段落 4 来了解 signed integer 溢出是未定义的行为:

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

我们知道标准说未定义的行为是不可预测的,从定义附带的注释中可以看出:

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

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

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 kernel null pointer check removal 非常相似,其中看到了这段代码:

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

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

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


我知道这是编译器编写者正在做的事情(我曾经编写过编译器,甚至是一两个优化器),但是有些行为是“有用的”,即使它们是“未定义的”,这也朝着更积极的优化方向发展只是精神错乱。您在上面引用的构造是错误的,但是优化错误检查是对用户不利的。
@JulieinAustin 我同意这是非常令人惊讶的行为,说开发人员需要避免未定义的行为实际上只是问题的一半。显然,编译器还需要向开发人员提供更好的反馈。在这种情况下,会产生一个警告,尽管它并没有提供足够的信息。
我认为这是一件好事,我想要更好、更快的代码。永远不应该使用UB。
@paulm 在道德上 UB 显然很糟糕,但很难与提供更好的 better tools(例如 clang static analyzer)争论,以帮助开发人员在 UB 和其他问题影响生产应用程序之前发现它。
@ShafikYaghmour 另外,如果您的开发人员忽略警告,那么他们会注意clang 输出的可能性有多大?这个问题很容易被激进的“没有不合理的警告”政策抓住。 Clang 是可取的,但不是必需的。
M
M.M

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 重新编译,那么 -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

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


最好弄清楚优化器的逻辑到底是什么,在这个阶段我不清楚为什么有些情况下优化了测试而有些没有
R
Ryan Bemrose

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

int a[50], x;

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

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

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


更神秘的是,只有在优化开启时才会发出消息。 M$ VB 消息“数组越界”是给假人用的吗?
C
Community

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

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

C11:1.3.24 未定义的行为:

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


@bits_international;是的。
你是对的,可以公平地解释我为什么投反对票。这个答案中的信息是正确的,但它没有教育意义,它完全忽略了房间里的大象:破损显然发生在与导致溢出的操作不同的地方(停止条件)。没有解释在这种特定情况下如何破坏事物的机制,尽管这是这个问题的核心。这是典型的不良教师情况,教师的回答不仅没有解决问题的核心,而且阻碍了进一步的问题。这几乎听起来像...
“我认为这是未定义的行为,从这一点开始,我不在乎它如何或为什么会破坏。标准允许它破坏。没有进一步的问题。”你可能不是那个意思,但看起来是这样的。我希望在 SO 上看到更少的这种(不幸的是常见的)态度。这实际上没有用。如果你得到用户输入,那么在每个有符号整数运算之后检查溢出是不合理的,即使标准说程序的任何其他部分都可能因此而崩溃。了解它是如何中断的确实有助于在实践中避免此类问题。
@Szabolcs:最好将 C 视为两种语言,其中一种旨在允许简单的编译器在程序员的帮助下实现合理高效的可执行代码,这些程序员利用在其预期目标平台上可靠但不可靠的构造其他的,因此被标准委员会忽略了,第二语言排除了标准不强制支持的所有此类结构,目的是允许编译器应用额外的优化,这些优化可能会或可能不会超过程序员必须做的放弃。
@Szabolcs“如果您获得用户输入,则在每次单个有符号整数运算后检查溢出是不合理的” - 正确,因为那时为时已晚。您必须在每个有符号整数运算之前检查溢出。