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


129

以下代码在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,因为我假设此错误将在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

10
如果您包含从中生成的汇编代码,这将更加负责gcc -S
2011年

组装非常长。我还应该编辑它吗?
Mysticial

请只提供与循环相关的部分。
2011年

12
-1。您说严格来说这是未定义的行为,请问这是否是未定义的行为。所以这对我来说不是一个真正的问题。
Johannes Schaub-litb

8
@ JohannesSchaub-litb感谢您的评论。我的措辞可能不好。我会尽力澄清赚取您未投票的票(然后我将相应地编辑问题)。基本上,我知道它是UB。但是我也知道x86上的GCC使用x86整数指令-溢出时自动换行。因此,尽管它是UB ,但我希望它能包裹起来。但是,事实并非如此,这让我感到困惑。因此是一个问题。
Mysticial

Answers:


178

当标准说这是未定义的行为时,就意味着它。什么都可能发生。“任何内容”都包括“通常是整数,但有时会发生奇怪的事情”。

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


24
哇哦 我不知道-fwrapv。感谢您指出了这一点。
Mysticial 2011年

1
是否有一个警告选项可以尝试注意意外的无限循环?
Jeff Burdges

5
:我发现-Wunsafe环的优化这里提到stackoverflow.com/questions/2982507/...
杰夫Burdges

1
-1“是的,在x86 CPU上,整数通常按照您期望的方式包装。” 错了 但这很微妙。我记得有可能使它们陷入溢出,但这不是我们在这里谈论的,而且我从未见过这样做。除此之外,不考虑x86 bcd操作(在C ++中不允许表示),x86整数操作总是包装,因为它们是二进制补码。您会误以为x86整数操作的属性对g ++进行了错误的(或极其不切实际且毫无意义的)优化。
干杯和健康。-阿尔夫

5
@ Cheersandhth.-Alf,“在x86 CPU上”,我的意思是“当您使用C编译器为x86 CPU开发时”。我真的需要说明吗?显然,如果您使用汇编程序进行开发,那么我关于编译器和GCC的所有讨论都是无关紧要的,在这种情况下,整数溢出的语义确实定义得很好。
bdonlan 2012年

18

很简单:未定义的行为-尤其是-O2在启用优化()的情况下- 可能发生任何事情。

您的代码在没有-O2切换的情况下按预期运行。

顺便说一下,它在icl和tcc上都可以很好地工作,但是您不能依靠这种东西...

根据,gcc的优化其实功勋整数溢出签署。这将意味着“错误”是设计使然。


有点遗憾的是,编译器会为不确定的行为选择所有事物的无限循环。

27
@逆:我不同意。如果您编写的代码具有未定义的行为,请为无限循环祈祷。使其更易于检测……
丹尼斯

我的意思是,如果编译器正在积极寻找UB,为什么不插入异常而不是尝试对破碎的代码进行超优化?

15
@Inverse:编译器不会主动寻找未定义的行为,它假定不会发生。这使编译器可以优化代码。例如,for (j = i; j < i + 10; ++j) ++k;它只设置set ,而不是计算,k = 10因为如果没有签名的溢出发生,它将始终为true。
丹尼斯,

@Inverse编译器没有“选择”任何东西。您在代码中编写了循环。编译器没有发明它。
2013年

13

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

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



3

拜托人们,未定义的行为正是undefined。这意味着任何事情都可能发生。在实践中(如本例所示),编译器可以自由假设它不会被调用,并尽一切可能使代码更快/更小。任何人都应该猜测,不应运行的代码会发生什么。这将取决于周围的代码(依赖于此,编译器可能会生成不同的代码),使用的变量/常量,编译器标志……。哦,编译器可能会更新并以不同的方式编写相同的代码,或者您可以获得另一个对代码生成有不同看法的编译器。或只是获得一台不同的机器,即使是同一体系结构系列中的另一个模型也可能具有其自己的未定义行为(查找未定义的操作码,一些进取的程序员发现,在某些早期的机器上有时确实做了有用的事情...) 。有没有“编译器对未定义的行为给出确定的行为”。有些区域是实现定义的,您应该可以依靠它们表现出一致的表现。


1
是的,我非常清楚什么是未定义的行为。但是,当您知道该语言的某些方面是如何为特定环境实现的时,您可以期望看到某些类型的UB,而不是其他。我知道GCC将整数算术实现为x86整数算术-溢出时自动换行。所以我就这样假设行为。我没想到的是,正如bdonlan回答的那样,GCC还会做其他事情。
Mysticial 2013年

7
错误。发生的情况是,允许GCC假定您不会调用未定义的行为,因此它只会发出代码,就像无法发生一样。如果确实发生了,则执行没有任何未定义行为的要求的指令,结果就是CPU 要做的任何事情。即,在x86上是x86的东西。如果是另一个处理器,它可能会做完全不同的事情。或者,编译器可能足够聪明,以至于发现您正在调用未定义的行为并启动nethack(是的,某些gcc的较早版本确实做到了这一点)。
vonbrand 2013年

4
我相信您误解了我的评论。我说:“没想到的事”-这就是为什么我首先问这个问题的原因。我想到海湾合作委员会会采取任何措施。
Mysticial 2013年

1

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

允许溢出产生“部分不确定的值”的规则将得到充分定义,以遵守附件L的文字和精神,但不会阻止编译器做出与普遍使用的推断相同的推断(如果溢出不受限制的话)未定义的行为。这将阻止编译器进行一些虚假的“优化”,而这些伪造的“优化”在许多情况下的主要作用是要求程序员在代码中添加额外的混乱,其唯一目的是防止此类“优化”。那是否是一件好事取决于一个人的观点。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.