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

Mys*_*ial 124 c c++ x86 gcc 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;
}
Run Code Online (Sandbox Code Playgroud)

所以这是交易:有符号整数溢出在技术上是未定义的行为.但是x86上的GCC使用x86整数指令实现整数运算 - 它包含溢出.

因此,我本来期望它包装溢出 - 尽管事实上它是未定义的行为.但事实显然并非如此.那么我错过了什么?

我使用以下方法编译:

~/Desktop$ g++ main.cpp -O2
Run Code Online (Sandbox Code Playgroud)

GCC输出:

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

... (infinite loop)
Run Code Online (Sandbox Code Playgroud)

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

正确的输出:

~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3
Run Code Online (Sandbox Code Playgroud)

以下是一些其他变体:

i *= 2;   //  Also fails and goes into infinite loop.
i <<= 1;  //  This seems okay. It does not enter infinite loop.
Run Code Online (Sandbox Code Playgroud)

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

~/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$ 
Run Code Online (Sandbox Code Playgroud)

所以问题是:这是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
Run Code Online (Sandbox Code Playgroud)

bdo*_*lan 173

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

是的,在x86 CPU上,整数通常以您期望的方式包装.这是其中一个例外.编译器假定您不会导致未定义的行为,并优化掉循环测试.如果你真的想要环绕,传递-fwrapvg++gcc编译时; 这为您提供了明确定义的(二进制补码)溢出语义,但可能会损害性能.

  • 哇哇 我不知道`-fwrapv`.感谢您指出了这一点. (24认同)
  • 我发现-Wunsafe环的优化这里提到:http://stackoverflow.com/questions/2982507/what-is-the-explanation-for-warning-assuming-that-the-loop-is-not-infinite (5认同)
  • @ Cheersandhth.-Alf,'on x86 CPUs'我的意思是"当你使用C编译器开发x86 CPU时".我真的需要拼出来吗?显然,如果你在汇编程序中进行开发,我所有关于编译器和GCC的讨论都是无关紧要的,在这种情况下,整数溢出的语义确实非常明确. (4认同)
  • 是否有一个警告选项试图注意到意外的无限循环? (2认同)

Den*_*nis 18

这很简单:未定义的行为 - 尤其是-O2启用了optimization()时 - 意味着任何事情都可能发生.

您的代码表现为(您)没有-O2切换的预期.

顺便说一下,它对icl和tcc工作得很好,但你不能依赖这样的东西......

根据,gcc的优化其实功勋符号整数溢出.这意味着"bug"是设计的.

  • @Inverse:我不同意.如果您已经编写了具有未定义行为的内容,请为无限循环祈祷.使检测更容易...... (27认同)
  • @Inverse:编译器不主动寻找未定义的行为*,它假定它不会发生.这允许编译器优化代码.例如,不是计算`for(j = i; j <i + 10; ++ j)++ k;`,它只会设置`k = 10`,因为如果**将总是**为真没有签名溢出. (15认同)
  • @Inverse http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html (4认同)

Man*_*rse 11

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

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


los*_*yzd 5

i += i;
Run Code Online (Sandbox Code Playgroud)

// 溢出未定义。

使用 -fwrapv 是正确的。-fwrapv


von*_*and 5

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

  • 错误的。发生的情况是,GCC 可以假设您不会调用未定义的行为,因此它只是发出代码*就好像*它不会发生一样。如果它“确实”发生,则执行您所要求的指令,并且“无”未定义行为,并且结果是 CPU 所做的任何事情。即,在 x86 上是做 x86 的事情。如果是另一个处理器,它可能会做完全不同的事情。或者编译器可能足够聪明,可以发现您正在调用未定义的行为并启动 nethack(是的,某些古老版本的 gcc 正是这样做的)。 (9认同)
  • 我相信你误读了我的评论。我说:“我没想到”——这就是我首先问这个问题的原因。我“没想到”海湾合作委员会会耍花招。 (5认同)