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

时间:2011-10-07 02:24:43

标签: c++ c gcc x86 undefined-behavior

以下代码在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

GCC输出:

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

... (infinite loop)

禁用优化后,没有无限循环且输出正确。 Visual Studio也正确编译它并给出以下结果:

正确输出:

~/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$ 

所以问题是:这是GCC中的错误吗?或者我误解了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

6 个答案:

答案 0 :(得分:169)

当标准说它是未定义的行为时,就意味着它。任何事情都可能发生。 “任何东西”包括“通常整数环绕,但偶尔会发生奇怪的事情”。

是的,在x86 CPU上,整数通常以你期望的方式包装。 This is one of those exceptions.编译器假定您不会导致未定义的行为,并优化掉循环测试。如果您真的想要回绕,请在编译时将-fwrapv传递给g++gcc;这给你定义明确的(二进制补码)溢出语义,但会损害性能。

答案 1 :(得分:18)

很简单:未定义的行为 - 特别是在启用优化(-O2)的情况下 - 意味着任何都可能发生。

您的代码在没有-O2开关的情况下表现为(您)。

顺便说一下,icl和tcc的效果还算不错,但你不能依赖这样的东西......

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

答案 2 :(得分:11)

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

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

答案 3 :(得分:4)

i += i;

//溢出未定义。

使用-fwrapv是正确的。 -fwrapv

答案 4 :(得分:3)

请大家,未定义的行为就是这样, undefined 。这意味着任何事情都可能发生。在实践中(如本例所示),编译器可以自由地假设不会被调用,并且如果可以使代码更快/更小,那就做任何它喜欢的事情。不应该运行的代码会发生什么,这是任何人的猜测。它将取决于周围的代码(取决于它,编译器可以很好地生成不同的代码),使用的变量/常量,编译器标志,......哦,编译器可以更新并以不同的方式编写相同的代码,或者你可以在代码生成上获得另一个具有不同视图的编译器。或者只是获得一台不同的机器,即使是同一架构系列中的另一个机型也可能有它自己的未定义行为(查找未定义的操作码,一些有进取心的程序员发现,在某些早期机器上有时会做有用的东西...) 。 no “编译器对未定义的行为给出明确的行为”。有些区域是实现定义的,您应该能够依赖编译器的一致行为。

答案 5 :(得分:1)

即使编译器要指定整数溢出必须被视为未定义行为的“非关键”形式(如附件L中所定义),整数溢出的结果应该是没有特定平台的更具体的承诺行为,至少被视为“部分不确定的价值”。根据这样的规则,添加1073741824 + 1073741824可以任意地视为产生2147483648或-2147483648或与2147483648 mod 4294967296一致的任何其他值,并且通过添加获得的值可以任意地被视为与0 mod 4294967296一致的任何值。

允许溢出产生“部分不确定值”的规则将被充分明确地定义为遵守附件L的字母和精神,但不会阻止编制者做出与合理的一般有用的推论。溢出是不受约束的未定义行为。它会阻止编译器做出一些虚假的“优化”,它们在许多情况下的主要作用是要求程序员为代码增加额外的混乱,其唯一的目的是阻止这种“优化”;这是否是一件好事取决于一个人的观点。