web-dev-qa-db-ja.com

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

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
125
Mysticial

標準が未定義の動作であると言うとき、それはそれを意味します。何でも起れる。 「すべて」には「通常は整数がラップアラウンドしますが、場合によっては奇妙なことが起こります」も含まれます。

はい、x86 CPUでは、整数は通常期待どおりにラップします。 これはそれらの例外の1つです。 コンパイラは、ユーザーが未定義の動作を引き起こさないと想定し、ループテストを最適化します。ラップアラウンドが本当に必要な場合は、-fwrapvからg++またはgccコンパイル時。これにより、明確に定義された(2の補数)オーバーフローセマンティクスが得られますが、パフォーマンスが低下する可能性があります。

171
bdonlan

簡単です:未定義の動作-特に最適化(-O2)がオンの場合-anythingが発生する可能性があることを意味します。

あなたのコードは、-O2スイッチなしで期待通りに動作します。

ちなみにiclとtccでうまく動作しますが、そのようなものに頼ることはできません...

this によると、gccの最適化は符号付き整数オーバーフローを実際に活用します。これは、「バグ」が仕様によるものであることを意味します。

18
Dennis

ここで注意すべき重要なことは、C++プログラムはC++抽象マシン(通常はハードウェア命令によってエミュレートされる)用に作成されていることです。 x86用にコンパイルしているという事実はtotallyこれが未定義の動作をしているという事実とは無関係です。

コンパイラは、未定義の動作の存在を自由に使用して、最適化を改善します(この例のように、ループから条件を削除します)。実行時にマシンコードがC++抽象マシンが要求する結果を生成するという要件を除いて、C++レベルの構成体とx86レベルのマシンコード構成体の間のマッピングは保証されておらず、有用ですらありません。

11
Mankarse
i += i;

//オーバーフローは未定義です。

-fwrapvを使用すると正しいです。 -fwrapv

4
lostyzd

ndefined behaviourはまさにndefinedです。それは何でも起こり得ることを意味します。実際には(この場合のように)、コンパイラーはそれを自由に想定できますwo n'tが呼び出され、コードの高速化/小型化が可能であれば、好きなことを行います。実行すべきではないコードで何が起こるかは、だれかの推測です。それは周囲のコード(それに応じて、コンパイラは異なるコードを生成する可能性があります)、使用される変数/定数、コンパイラフラグに依存します...ああ、コンパイラは更新されて同じコードを異なる方法で書くことができますコード生成に関して異なる視点を持つ別のコンパイラを入手してください。または、単に別のマシンを入手するだけで、同じアーキテクチャーラインの別のモデルでも独自の未定義の動作を行うことができます(未定義のオペコードを調べてください。 。 no「コンパイラは未定義の動作に対して明確な動作を提供します」があります。実装定義の領域があり、そこではコンパイラが一貫して動作することを期待できるはずです。

3
vonbrand

コンパイラが整数オーバーフローを「非クリティカルな」未定義動作(Annex Lで定義)の形式と見なす必要があると指定した場合でも、整数オーバーフローの結果は、特定のプラットフォームがより具体的な動作を約束しない限り、少なくとも「部分的に不定の値」と見なされます。そのような規則の下では、1073741824 + 1073741824を追加すると、2147483648または-2147483648、または2147483648 mod 4294967296に一致する他の値が得られると任意に見なされ、加算によって取得される値は0 mod 4294967296に一致する任意の値と任意に見なされる可能性があります。

「部分的に不定な値」を生成するオーバーフローを許可するルールは、Annex Lの文字と精神を順守するために十分に明確に定義されますが、コンパイラーがオーバーフローが制約されない場合に正当化されるのと同じ一般的に有用な推論を行うことを妨げません未定義の動作。多くの場合、コンパイラがそのような「最適化」を防止することを唯一の目的とするコードに余分な混乱を加えることを主な効果とする偽の「最適化」をコンパイラが行うことを防ぎます。それが良いことであるかどうかは、自分の視点に依存します。

1
supercat