web-dev-qa-db-ja.com

このループが「警告:反復3uが未定義の動作を呼び出します」を生成し、4行以上を出力するのはなぜですか?

これをコンパイルする:

#include <iostream>

int main()
{
    for (int i = 0; i < 4; ++i)
        std::cout << i*1000000000 << std::endl;
}

gccは次の警告を生成します。

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^

符号付き整数オーバーフローがあることを理解しています。

私が得ることができないのは、なぜi値がそのオーバーフロー操作によって壊れているのですか?

GCCを使用したx86で整数オーバーフローが原因で無限ループが発生するのはなぜですか? の回答を読みましたが、whyこれは起こります-「未定義」は「何でも起こり得る」ことを意味しますが、この特定の動作の根本的な原因は何ですか

オンライン: http://ideone.com/dMrRKR

コンパイラ:gcc (4.8)

156
zerkms

符号付き整数オーバーフロー(厳密に言えば、「符号なし整数オーバーフロー」などはありません)は、undefined behaviourを意味します。そして、これは何でも起こり得ることを意味し、C++のルールの下でそれが起こる理由を議論することは意味がありません。

C++ 11ドラフトN3337:§5.4:1

式の評価中に、結果が数学的に定義されていない場合、またはその型の表現可能な値の範囲内にない場合、動作は未定義です。 [注:C++の既存の実装のほとんどは、整数オーバーフローを無視します。ゼロ除算の処理、ゼロ除数を使用した剰余の形成、およびすべての浮動小数点例外はマシンによって異なり、通常はライブラリ関数によって調整可能です。 —注を終了]

g++ -O3でコンパイルされたコードは警告を発します(-Wallがなくても)

a.cpp: In function 'int main()':
a.cpp:11:18: warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^
a.cpp:9:2: note: containing loop
  for (int i = 0; i < 4; ++i)
  ^

プログラムが何をしているかを分析できる唯一の方法は、生成されたアセンブリコードを読み取ることです。

完全なアセンブリリストは次のとおりです。

    .file   "a.cpp"
    .section    .text$_ZNKSt5ctypeIcE8do_widenEc,"x"
    .linkonce discard
    .align 2
LCOLDB0:
LHOTB0:
    .align 2
    .p2align 4,,15
    .globl  __ZNKSt5ctypeIcE8do_widenEc
    .def    __ZNKSt5ctypeIcE8do_widenEc;    .scl    2;  .type   32; .endef
__ZNKSt5ctypeIcE8do_widenEc:
LFB860:
    .cfi_startproc
    movzbl  4(%esp), %eax
    ret $4
    .cfi_endproc
LFE860:
LCOLDE0:
LHOTE0:
    .section    .text.unlikely,"x"
LCOLDB1:
    .text
LHOTB1:
    .p2align 4,,15
    .def    ___tcf_0;   .scl    3;  .type   32; .endef
___tcf_0:
LFB1091:
    .cfi_startproc
    movl    $__ZStL8__ioinit, %ecx
    jmp __ZNSt8ios_base4InitD1Ev
    .cfi_endproc
LFE1091:
    .section    .text.unlikely,"x"
LCOLDE1:
    .text
LHOTE1:
    .def    ___main;    .scl    2;  .type   32; .endef
    .section    .text.unlikely,"x"
LCOLDB2:
    .section    .text.startup,"x"
LHOTB2:
    .p2align 4,,15
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB1084:
    .cfi_startproc
    leal    4(%esp), %ecx
    .cfi_def_cfa 1, 0
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    .cfi_escape 0x10,0x5,0x2,0x75,0
    movl    %esp, %ebp
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    pushl   %ecx
    .cfi_escape 0xf,0x3,0x75,0x70,0x6
    .cfi_escape 0x10,0x7,0x2,0x75,0x7c
    .cfi_escape 0x10,0x6,0x2,0x75,0x78
    .cfi_escape 0x10,0x3,0x2,0x75,0x74
    xorl    %edi, %edi
    subl    $24, %esp
    call    ___main
L4:
    movl    %edi, (%esp)
    movl    $__ZSt4cout, %ecx
    call    __ZNSolsEi
    movl    %eax, %esi
    movl    (%eax), %eax
    subl    $4, %esp
    movl    -12(%eax), %eax
    movl    124(%esi,%eax), %ebx
    testl   %ebx, %ebx
    je  L15
    cmpb    $0, 28(%ebx)
    je  L5
    movsbl  39(%ebx), %eax
L6:
    movl    %esi, %ecx
    movl    %eax, (%esp)
    addl    $1000000000, %edi
    call    __ZNSo3putEc
    subl    $4, %esp
    movl    %eax, %ecx
    call    __ZNSo5flushEv
    jmp L4
    .p2align 4,,10
L5:
    movl    %ebx, %ecx
    call    __ZNKSt5ctypeIcE13_M_widen_initEv
    movl    (%ebx), %eax
    movl    24(%eax), %edx
    movl    $10, %eax
    cmpl    $__ZNKSt5ctypeIcE8do_widenEc, %edx
    je  L6
    movl    $10, (%esp)
    movl    %ebx, %ecx
    call    *%edx
    movsbl  %al, %eax
    pushl   %edx
    jmp L6
L15:
    call    __ZSt16__throw_bad_castv
    .cfi_endproc
LFE1084:
    .section    .text.unlikely,"x"
LCOLDE2:
    .section    .text.startup,"x"
LHOTE2:
    .section    .text.unlikely,"x"
LCOLDB3:
    .section    .text.startup,"x"
LHOTB3:
    .p2align 4,,15
    .def    __GLOBAL__sub_I_main;   .scl    3;  .type   32; .endef
__GLOBAL__sub_I_main:
LFB1092:
    .cfi_startproc
    subl    $28, %esp
    .cfi_def_cfa_offset 32
    movl    $__ZStL8__ioinit, %ecx
    call    __ZNSt8ios_base4InitC1Ev
    movl    $___tcf_0, (%esp)
    call    _atexit
    addl    $28, %esp
    .cfi_def_cfa_offset 4
    ret
    .cfi_endproc
LFE1092:
    .section    .text.unlikely,"x"
LCOLDE3:
    .section    .text.startup,"x"
LHOTE3:
    .section    .ctors,"w"
    .align 4
    .long   __GLOBAL__sub_I_main
.lcomm __ZStL8__ioinit,1,1
    .ident  "GCC: (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.0"
    .def    __ZNSt8ios_base4InitD1Ev;   .scl    2;  .type   32; .endef
    .def    __ZNSolsEi; .scl    2;  .type   32; .endef
    .def    __ZNSo3putEc;   .scl    2;  .type   32; .endef
    .def    __ZNSo5flushEv; .scl    2;  .type   32; .endef
    .def    __ZNKSt5ctypeIcE13_M_widen_initEv;  .scl    2;  .type   32; .endef
    .def    __ZSt16__throw_bad_castv;   .scl    2;  .type   32; .endef
    .def    __ZNSt8ios_base4InitC1Ev;   .scl    2;  .type   32; .endef
    .def    _atexit;    .scl    2;  .type   32; .endef

Assemblyを読むことさえほとんどできませんが、addl $1000000000, %edi行を見ることができます。結果のコードは次のようになります

for(int i = 0; /* nothing, that is - infinite loop */; i += 1000000000)
    std::cout << i << std::endl;

@ T.C。のこのコメント:

(1)2より大きい値のiを持つすべての反復には未定義の動作があるため->(2)最適化のためにi <= 2と仮定できる->(3)ループ条件は常に真である-> (4)無限ループに最適化されます。

oPのコードのアセンブリコードを、未定義の動作なしで、次のコードのアセンブリコードと比較するというアイデアを私に与えてくれました。

#include <iostream>

int main()
{
    // changed the termination condition
    for (int i = 0; i < 3; ++i)
        std::cout << i*1000000000 << std::endl;
}

そして実際、正しいコードには終了条件があります。

    ; ...snip...
L6:
    mov ecx, edi
    mov DWORD PTR [esp], eax
    add esi, 1000000000
    call    __ZNSo3putEc
    sub esp, 4
    mov ecx, eax
    call    __ZNSo5flushEv
    cmp esi, -1294967296 // here it is
    jne L7
    lea esp, [ebp-16]
    xor eax, eax
    pop ecx
    ; ...snip...

OMG、それは完全に明らかではありません!それは公平ではありません!火事による裁判を要求します!

それに対処し、バグのあるコードを書いたので、気分が悪くなるはずです。結果に耐える。

...または、代わりに、より良い診断とより良いデバッグツールを適切に使用します-それが彼らのためです:

  • すべての警告を有効にする

    • -Wallは、誤検知のない便利な警告をすべて有効にするgccオプションです。これは、最低限使用する必要がある最低限のものです。
    • gccには他の多くの警告オプションがあります 、ただし、-Wallでは有効ではありません。誤検知の警告が表示される可能性があるためです。
    • 残念ながら、Visual C++は有用な警告を出す機能に遅れをとっています。少なくともIDEはデフォルトでいくつかを有効にします。
  • デバッグにデバッグフラグを使用する

    • 整数オーバーフローの場合、-ftrapvはオーバーフロー時にプログラムをトラップします。
    • これにはClangコンパイラーが優れています:-fcatch-undefined-behaviorは、未定義の動作の多くのインスタンスをキャッチします(注:"a lot of" != "all of them"

明日出荷する必要のある、私が書いたのではないプログラムのスパゲッティ混乱があります!ヘルプ!!!!!! 111oneone

Gccの-fwrapvを使用

このオプションは、加算、減算、乗算の符号付き算術オーバーフローが2の補数表現を使用して折り返すことを想定するようにコンパイラーに指示します。

1 -§3.9.1.4にあるように、この規則は「符号なし整数オーバーフロー」には適用されません

符号なしと宣言された符号なし整数は、2を法とする算術の法則に従います。n ここで、nは整数の特定のサイズの値表現のビット数です。

そして、例えばUINT_MAX + 1の結果は数学的に定義されます-2を法とする算術の規則によってn

104
milleniumbug

短い答え、gccはこの問題を具体的に文書化していますが、 gcc 4.8リリースノート に(emphasis mine going forward)と書かれていることがわかります:

GCCは、より多くの言語標準によって課せられた制約を使用してループの反復回数の上限を導出するための積極的な分析を使用するようになりました。これにより、SPEC CPU 2006 464.h264refや416.gamessなどの不適合プログラムが期待どおりに動作しなくなる場合があります。この積極的な分析を無効にするために、新しいオプション-fno-aggressive-loop-optimizationsが追加されました。一定の反復回数はわかっているが、最後の反復に到達する前または最後にループ内で未定義の動作が発生することがわかっている一部のループでは、GCCは反復数の上限を導出する代わりにループ内の未定義の動作について警告しますループ用。警告は、-Wno-aggressive-loop-optimizationsで無効にできます。

実際、-fno-aggressive-loop-optimizationsを使用すると、無限ループの動作は停止し、テストしたすべてのケースで停止します。

長い答えは、ドラフトC++標準セクション5Expressionsを見て、signed integerオーバーフローが未定義の動作であることを知ることから始まります。 /パラグラフ4

式の評価中に結果が数学的に定義されていないか、そのタイプの表現可能な値の範囲にない場合、動作は未定義です。 [注:C++の既存の実装のほとんどは、整数オーバーフローを無視します。ゼロ除算の処理、ゼロ除数を使用した剰余の形成、およびすべての浮動小数点例外はマシンによって異なり、通常はライブラリ関数によって調整できます。 —メモを終了

標準では、未定義の動作は次のように定義されている注記から予測不可能であるとされています。

[注:この国際標準が動作の明示的な定義を省略している場合、またはプログラムが誤った構造または誤ったデータを使用している場合、未定義の動作が予想されることがあります。 許容されない未定義の動作範囲は、予測不可能な結果を​​伴う状況の完全な無視から、環境に特有の文書化された方法での動作中の動作(診断メッセージの発行の有無にかかわらず)、翻訳または実行(診断メッセージの発行を伴う)。多くの誤ったプログラム構造は、未定義の動作を引き起こしません。それらは診断される必要があります。 —注を終了]

しかし、これを無限ループに変えるためにgccオプティマイザーは何をすることができますか?それは完全に奇抜に聞こえます。しかし、ありがたいことにgccは警告でそれを理解する手がかりを与えてくれます。

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^

手がかりはWaggressive-loop-optimizationsですが、それはどういう意味ですか?幸いなことに、この最適化がこの方法でコードを壊したのはこれが初めてではなく、幸運です。JohnRegehrが記事でケースを文書化したためです GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks 次のコードを示します。

int d[16];

int SATD (void)
{
  int satd = 0, dd, k;
  for (dd=d[k=0]; k<16; dd=d[++k]) {
    satd += (dd < 0 ? -dd : dd);
  }
  return satd;
}

記事によると:

未定義の動作は、ループを終了する直前にd [16]にアクセスしています。 C99では、配列の末尾から1つ上の位置にある要素へのポインターを作成することはできますが、そのポインターを逆参照してはなりません。

後で言う:

詳細は、次のとおりです。 Cコンパイラは、d [++ k]を見ると、未定義の動作が発生するため、インクリメントされたkの値が配列境界内にあると想定できます。ここのコードでは、GCCはkが0..15の範囲にあると推測できます。少し後で、GCCがk <16を認識すると、「あぁ–その式は常に真であるため、無限ループになります。」ここで、コンパイラーは明確な定義の仮定を使用します有用なデータフローの事実を推測するには、

そのため、コンパイラが実行しなければならないことは、符号付き整数のオーバーフローが未定義の動作であるため、iが常に4より小さくなければならず、したがって無限ループがあることを前提としています。

彼はこれが悪名高い LinuxカーネルのNULLポインターチェックの削除 に非常に似ていると説明しています。

struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;

gccは、ss->f;で参照され、nullポインターの参照解除は未定義の動作であるため、sはnullであってはならず、したがって、次の行のif (!s)チェックを最適化することを推測しました。

ここでの教訓は、最新のオプティマイザーは未定義の動作を悪用することに非常に積極的であり、ほとんどの場合より積極的になることです。明らかに、ほんの数例で、オプティマイザーがプログラマーにとって完全に不合理に見えることをしているのを見ることができますが、オプティマイザーの観点から見れば意味があります。

65
Shafik Yaghmour

tl; drコードは、integer+正の整数==負の整数。通常、オプティマイザーはこれを最適化しませんが、std::endlが次に使用される特定のケースでは、コンパイラーはこのテストを最適化します。 endlの特別な点はまだわかりません。


-O1以上のレベルのアセンブリコードから、gccがループを次のようにリファクタリングすることは明らかです。

i = 0;
do {
    cout << i << endl;
    i += NUMBER;
} 
while (i != NUMBER * 4)

正しく機能する最大の値は715827882、つまりfloor(INT_MAX/3)です。 -O1のアセンブリスニペットは次のとおりです。

L4:
movsbl  %al, %eax
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
addl    $715827882, %esi
cmpl    $-1431655768, %esi
jne L6
    // fallthrough to "return" code

注、-1431655768は2の補数の4 * 715827882です。

-O2を押すと、次のように最適化されます。

L4:
movsbl  %al, %eax
addl    $715827882, %esi
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
cmpl    $-1431655768, %esi
jne L6
leal    -8(%ebp), %esp
jne L6 
   // fallthrough to "return" code

そのため、行われた最適化は、単にaddlが上に移動しただけです。

代わりに715827883を使用して再コンパイルすると、-O1バージョンは変更された数値とテスト値を除いて同一です。ただし、-O2は変更を行います。

L4:
movsbl  %al, %eax
addl    $715827883, %esi
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
jmp L2

cmpl $-1431655764, %esi-O1があった場合、-O2の行は削除されました。オプティマイザーは、715827883%esiに追加しても-1431655764に等しくならないことを決定している必要があります。

これはかなり不可解です。それをINT_MIN+1に追加するとdoes期待される結果が生成されるため、オプティマイザーは%esiINT_MIN+1にならないことを決定している必要があり、なぜ決定するのかわかりませんそれ。

作業例では、数値に715827882を追加するとINT_MIN + 715827882 - 2に等しくなり得ないと結論付けることも同様に有効であると思われます。 (これは、実際にラップアラウンドが発生する場合にのみ可能です)、それでもその例ではラインアウトを最適化しません。


私が使用していたコードは次のとおりです。

#include <iostream>
#include <cstdio>

int main()
{
    for (int i = 0; i < 4; ++i)
    {
        //volatile int j = i*715827883;
        volatile int j = i*715827882;
        printf("%d\n", j);

        std::endl(std::cout);
    }
}

std::endl(std::cout)が削除されると、最適化は行われなくなります。実際、std::endlがインライン化されていても、std::cout.put('\n'); std::flush(std::cout);で置き換えると最適化は行われません。

std::endlのインライン化は、ループ構造の初期の部分に影響を与えるようです(これが何をしているのかはよくわかりませんが、誰か他の人が行った場合に備えてここに投稿します)。

元のコードと-O2を使用:

L2:
movl    %esi, 28(%esp)
movl    28(%esp), %eax
movl    $LC0, (%esp)
movl    %eax, 4(%esp)
call    _printf
movl    __ZSt4cout, %eax
movl    -12(%eax), %eax
movl    __ZSt4cout+124(%eax), %ebx
testl   %ebx, %ebx
je  L10
cmpb    $0, 28(%ebx)
je  L3
movzbl  39(%ebx), %eax
L4:
movsbl  %al, %eax
addl    $715827883, %esi
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
jmp L2                  // no test

std::endl-O2を手動でインライン化すると:

L3:
movl    %ebx, 28(%esp)
movl    28(%esp), %eax
addl    $715827883, %ebx
movl    $LC0, (%esp)
movl    %eax, 4(%esp)
call    _printf
movl    $10, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    $__ZSt4cout, (%esp)
call    __ZNSo5flushEv
cmpl    $-1431655764, %ebx
jne L3
xorl    %eax, %eax

これら2つの違いは、オリジナルでは%esiが使用され、2番目のバージョンでは%ebxが使用されることです。一般的に%esi%ebxの間に定義されたセマンティクスに違いはありますか? (x86アセンブリについてはあまり知りません)。

23
M.M

Gccで報告されるこのエラーの別の例は、一定の反復回数実行するループがあるが、以下のようなアイテムの数が少ない配列へのインデックスとしてカウンター変数を使用している場合です。

int a[50], x;

for( i=0; i < 1000; i++) x = a[i];

コンパイラは、このループが配列「a」の外部のメモリにアクセスしようとすることを決定できます。コンパイラは、このかなり不可解なメッセージでこれについて不平を言っています。

反復xxuは未定義の動作を呼び出します[-Werror = aggressive-loop-optimizations]

6
Ed Tyler

私が得ることができないのは、なぜそのオーバーフロー操作によって値が壊れているのですか?

4回目の反復で整数オーバーフローが発生するようです(i = 3の場合)。 signed整数オーバーフローは未定義の動作を呼び出します。この場合、何も予測できません。ループは4回だけ反復するか、無限または他の何かに進む可能性があります!
結果は、コンパイラごとに異なる場合があり、同じコンパイラの異なるバージョンでも異なる場合があります。

C11:1.3.24未定義の動作:

この国際規格が要件を課していない行動
[。 予測できない結果を伴う状況を完全に無視することから、環境に特有の文書化された方法での診断またはプログラム実行中の動作(診断メッセージの発行の有無にかかわらず)、翻訳または実行の終了まで、許容される未定義の動作範囲(診断メッセージの発行)。多くの誤ったプログラム構造は、未定義の動作を引き起こしません。それらは診断される必要があります。 —注を終了]

6
haccks