ChatGPT解决这个技术问题 Extra ChatGPT

为什么使用 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)

在禁用优化的情况下,没有无限循环并且输出是正确的。 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
C/C++ 中的有符号整数溢出是未定义的行为(无符号整数运算是模 $2^w$ 其中 $w$ 是字大小)。
-1。你说这严格来说是未定义的行为,并询问这是否是未定义的行为。所以这对我来说不是一个真正的问题。
@JohannesSchaub-litb 感谢您的评论。可能是我的措辞不好。我会尽力澄清以赢得您的反对(我会相应地编辑问题)。基本上,我知道它是UB。但我也知道 x86 上的 GCC 使用 x86 整数指令——溢出时换行。因此,尽管它是 UB,但我希望它能够包装。然而,它没有,这让我很困惑。因此问题。

b
bdonlan

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

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


@Mysticial,或者只使用无符号整数,其中溢出行为总是完美定义的。使用 <<= 现在可能会起作用,但不能保证在未来版本的 gcc 中会出现这种情况。
是否有尝试注意意外无限循环的警告选项?
我发现这里提到的 -Wunsafe-loop-optimizations :stackoverflow.com/questions/2982507/…
-1 “是的,在 x86 CPU 上,整数通常以您期望的方式包装。”这是错误的。但它很微妙。我记得有可能让它们在溢出时陷入陷阱,但这不是我们在这里谈论的,而且我从未见过这样做过。除此之外,忽略 x86 bcd 操作(C++ 中不允许表示)x86 整数操作总是换行,因为它们是二进制补码。您将 g++ 错误(或极其不切实际和胡说八道)的优化误认为是 x86 整数操作的属性。
@Cheersandhth.-Alf,“在 x86 CPU 上”我的意思是“当您使用 C 编译器为 x86 CPU 进行开发时”。我真的需要拼出来吗?显然,如果您在汇编程序中开发,我所有关于编译器和 GCC 的讨论都是无关紧要的,在这种情况下,整数溢出的语义确实非常明确。
D
Dennis

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

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

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

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


编译器会为未定义的行为选择所有事物的无限循环,这有点糟糕。
@Inverse:我不同意。如果您编写了具有未定义行为的代码,请为无限循环祈祷。更容易检测...
我的意思是,如果编译器正在积极寻找 UB,为什么不插入异常而不是尝试超优化损坏的代码?
@Inverse:编译器没有主动寻找未定义的行为,它假定它不会发生。这允许编译器优化代码。例如,它不会计算 for (j = i; j < i + 10; ++j) ++k;,而是只设置 k = 10,因为如果没有发生带符号溢出,这将始终为真。
@Inverse 编译器没有“选择”任何东西。您在代码中编写了循环。编译器没有发明它。
M
Mankarse

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

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


l
lostyzd
i += i;

// 溢出未定义。

使用 -fwrapv 是正确的。 -fwrapv


v
vonbrand

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


是的,我非常清楚未定义的行为是什么。但是,当您知道语言的某些方面是如何为特定环境实现的时,您可以期望看到某些类型的 UB 而不是其他类型。我知道 GCC 将整数算术实现为 x86 整数算术 - 它在溢出时包装。所以我假设这种行为。我没想到的是GCC会像bdonlan回答的那样做其他事情。
错误的。发生的情况是允许 GCC 假设您不会调用未定义的行为,因此它只是发出代码,就好像它不可能发生一样。如果确实发生了,则执行您所要求的没有未定义行为的指令,结果就是 CPU 所做的任何事情。即,在 x86 上是 x86 的东西。如果它是另一个处理器,它可以做一些完全不同的事情。或者编译器可能足够聪明,可以确定您正在调用未定义的行为并启动 nethack(是的,一些古老的 gcc 版本正是这样做的)。
我相信你误读了我的评论。我说:“我没想到”——这就是我首先提出这个问题的原因。我没想到 GCC 会耍花招。
s
supercat

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

允许溢出产生“部分不确定值”的规则将被充分定义以遵守附件 L 的文字和精神,但不会阻止编译器做出与溢出不受约束时相同的普遍有用的推论未定义的行为。它将防止编译器进行一些虚假的“优化”,这些“优化”在许多情况下的主要效果是要求程序员在代码中添加额外的混乱,其唯一目的是防止这种“优化”;这是否是一件好事取决于一个人的观点。