为什么使用 GCC 的 x86上的整数溢出会导致无限循环?

下面的代码在 GCC 上进入一个无限循环:

#include <iostream>
using namespace std;


int main(){
int i = 0x10000000;


int c = 0;
do{
c++;
i += i;
cout << i << endl;
}while (i > 0);


cout << c << endl;
return 0;
}

事情是这样的: 符号整数溢出在技术上是有未定义行为的。但是 x86上的 GCC 使用 x86整数指令来实现整数算法——它包装了溢出。

因此,我本以为它会在溢出的情况下结束——尽管事实上它是未定义行为的。但事实并非如此。我错过了什么?

我编译这个使用:

~/Desktop$ g++ main.cpp -O2

海湾合作委员会产出:

~/Desktop$ ./a.out
536870912
1073741824
-2147483648
0
0
0


... (infinite loop)

禁用优化后,就不存在无限循环,输出正确。VisualStudio 也正确地编译了这些代码,并得到以下结果:

正确输出:

~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3

下面是其他一些变化:

i *= 2;   //  Also fails and goes into infinite loop.
i <<= 1;  //  This seems okay. It does not enter infinite loop.

这是所有相关的版本信息:

~/Desktop$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ..


...


Thread model: posix
gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu4)
~/Desktop$

所以问题是: 这是海湾合作委员会的一个 bug 吗?还是我对 GCC 处理整数算术的方式有什么误解?

* 我也给这个 C 打上标签,因为我假设这个 bug 会在 C 中重现(我还没有验证它)

编辑:

下面是循环的组合: (如果我识别正确的话)

.L5:
addl    %ebp, %ebp
movl    $_ZSt4cout, %edi
movl    %ebp, %esi
.cfi_offset 3, -40
call    _ZNSolsEi
movq    %rax, %rbx
movq    (%rax), %rax
movq    -24(%rax), %rax
movq    240(%rbx,%rax), %r13
testq   %r13, %r13
je  .L10
cmpb    $0, 56(%r13)
je  .L3
movzbl  67(%r13), %eax
.L4:
movsbl  %al, %esi
movq    %rbx, %rdi
addl    $1, %r12d
call    _ZNSo3putEc
movq    %rax, %rdi
call    _ZNSo5flushEv
cmpl    $3, %r12d
jne .L5
16091 次浏览

很简单: 未定义的行为——特别是打开优化(-O2)——意味着 什么都行可能发生。

在没有 -O2开关的情况下,代码的行为与(您)预期的一样。

对于 ICL 和 tcc 来说都很好用,但是你不能依赖这些东西。

根据 这个,gcc 优化实际上利用了有符号整数溢出。这意味着“ bug”是设计好的。

当标准说它是未定义行为的时候。一切皆有可能。“任何东西”包括“通常整数包围,但有时怪异的东西发生”。

是的,在 x86 CPU 上,整数 通常按照您预期的方式包装。编译器假设你不会引起未定义行为,并优化掉循环测试。如果您真的想要包装,那么在编译时将 -fwrapv传递给 g++gcc; 这将提供定义良好的溢出语义(两个补语) ,但是可能会影响性能。

i += i;

//溢出未定义。

使用 -fwrapv 是正确的。 Fwrapv

这里需要注意的重要一点是,C + + 程序是为 C + + 抽象机器编写的(通常通过硬件指令进行模拟)。您正在为 x86编译的事实是 绝对的,这与它具有未定义行为的事实无关。

编译器可以自由地使用未定义行为的存在来改进其优化(通过从循环中删除条件,如本例所示)。除了要求机器代码在执行时产生 C + + 抽象机器所需的结果之外,C + + 级别构造和 x86级别机器代码构造之间没有保证,甚至没有实用的映射。

拜托大家,不明确的行为就是 未定义。意思是任何事情都有可能发生。在实践中(如本例中) ,编译器可以自由地假设调用它的是 不会,并且如果这样可以使代码更快/更小,则可以做任何它喜欢做的事情。对于不应该运行的代码会发生什么,谁也说不准。这将取决于周围的代码(取决于这一点,编译器可以很好地生成不同的代码) ,变量/常量使用,编译器标志,... 哦,编译器可以得到更新,写相同的代码不同,或者你可以得到另一个编译器不同的代码生成视图。或者只是得到一台不同的机器,甚至在同一个架构线上的另一个模型也可能有它自己的未定义行为(查找未定义的操作码,一些有进取心的程序员发现,在一些早期的机器上有时确实做有用的东西...)。还有 没有“编译器对未定义的行为给出明确的行为”。有些区域是实现定义的,在这些区域中,您应该能够指望编译器的行为保持一致。

即使编译器指定整数溢出必须被认为是一种“非关键”形式的未定义行为(如附件 L 所定义的) ,整数溢出的结果应该至少被认为是一个“部分不确定值”,如果没有一个特定的平台承诺更具体的行为。根据这些规则,加上1073741824 + 1073741824可以任意看作是产出2147483648或 -2147483648或任何其他值与2147483648 mod 4294967296相等,加上所得的值可以任意看作是任何值与0 mod 4294967296相等。

允许溢出产生“部分不确定值”的规则将得到充分明确的定义,以遵守附件 L 的文字和精神,但不会阻止编译器作出相同的一般有用的推论,如果溢出是不受限制的未定义行为,这种推论是合理的。它可以防止编译器进行一些虚假的“优化”,这些“优化”的主要效果在很多情况下是要求程序员在代码中添加额外的混乱,而这些代码的唯一目的就是防止这种“优化”; 这是好事还是坏事取决于一个人的观点。