web-dev-qa-db-ja.com

StanfordチュートリアルとGCCの間の競合

this movie(約38分)によると、同じローカル変数を持つ2つの関数がある場合、それらは同じスペースを使用します。したがって、次のプログラムは5gcc結果でコンパイル-1218960859。どうして?

プログラム:

#include <stdio.h>

void A()
{
    int a;
    printf("%i",a);
}

void B()
{
    int a;
    a = 5;
}

int main()
{
    B();
    A();
    return 0;
}

要求どおり、逆アセンブラからの出力は次のとおりです。

0804840c <A>:
 804840c:   55                      Push   ebp
 804840d:   89 e5                   mov    ebp,esp
 804840f:   83 ec 28                sub    esp,0x28
 8048412:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 8048415:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048419:   c7 04 24 e8 84 04 08    mov    DWORD PTR [esp],0x80484e8
 8048420:   e8 cb fe ff ff          call   80482f0 <printf@plt>
 8048425:   c9                      leave  
 8048426:   c3                      ret    

08048427 <B>:
 8048427:   55                      Push   ebp
 8048428:   89 e5                   mov    ebp,esp
 804842a:   83 ec 10                sub    esp,0x10
 804842d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048434:   c9                      leave  
 8048435:   c3                      ret    

08048436 <main>:
 8048436:   55                      Push   ebp
 8048437:   89 e5                   mov    ebp,esp
 8048439:   83 e4 f0                and    esp,0xfffffff0
 804843c:   e8 e6 ff ff ff          call   8048427 <B>
 8048441:   e8 c6 ff ff ff          call   804840c <A>
 8048446:   b8 00 00 00 00          mov    eax,0x0
 804844b:   c9                      leave  
 804844c:   c3                      ret    
 804844d:   66 90                   xchg   ax,ax
 804844f:   90                      nop
82
elyashiv

はい、はい、これは未定義の動作です、なぜなら初期化されていない変数を使用しているからです1

ただし、x86アーキテクチャでは2この実験はうまくいくはずです。値はスタックから「消去」されず、B()で初期化されないため、スタックフレームが同一であれば、同じ値がまだ存在しているはずです。

_int a_がvoid B()内でusedされていないため、コンパイラがそのコードを最適化し、5がスタック上のその場所に書き込まれることはありません。 B()にもprintfを追加してみてください-うまくいくかもしれません。

また、コンパイラフラグ(最適化レベル)もこの実験に影響を与える可能性があります。 _-O0_をgccに渡して、最適化を無効にしてみてください。

編集:_gcc -O0_(64ビット)を使用してコードをコンパイルしたところ、実際に、プログラムは5を出力します。実際、_-O0_がなくても機能しました。 32ビットのビルドは異なる動作をする場合があります。

免責事項:絶対にしないでくださいever「実際の」コードではこのようなものを使用してください!

1-これが正式に「UB」であるか、または単に予測できないかどうかについて、議論が続いています

2-また、x64、およびコールスタック(少なくともMMUを備えたもの)を使用する他のすべてのアーキテクチャ


それが機能しなかった理由を見てみましょう。これは32ビットで最もよく見られるので、_-m32_でコンパイルします。

_$ gcc --version
gcc (GCC) 4.7.2 20120921 (Red Hat 4.7.2-2)
_

_$ gcc -m32 -O0 test.c_(最適化は無効)でコンパイルしました。これを実行すると、ゴミが表示されます。

_$ objdump -Mintel -d ./a.out_を見る:

_080483ec <A>:
 80483ec:   55                      Push   ebp
 80483ed:   89 e5                   mov    ebp,esp
 80483ef:   83 ec 28                sub    esp,0x28
 80483f2:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 80483f5:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 80483f9:   c7 04 24 c4 84 04 08    mov    DWORD PTR [esp],0x80484c4
 8048400:   e8 cb fe ff ff          call   80482d0 <printf@plt>
 8048405:   c9                      leave  
 8048406:   c3                      ret    

08048407 <B>:
 8048407:   55                      Push   ebp
 8048408:   89 e5                   mov    ebp,esp
 804840a:   83 ec 10                sub    esp,0x10
 804840d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048414:   c9                      leave  
 8048415:   c3                      ret    
_

Bでは、コンパイラが0x10バイトのスタックスペースを予約し、_int a_変数を_[ebp-0x4]_で5に初期化したことがわかります。

ただし、Aでは、コンパイラは_int a_を_[ebp-0xc]_に配置しました。したがって、この場合、ローカル変数は同じ場所にありませんAにもprintf()呼び出しを追加すると、ABのスタックフレームが同じになり、_55_が出力されます。 。

130

未定義の動作です。初期化されていないローカル変数には不確定な値があり、それを使用すると未定義の動作が発生します。

36

覚えておくべき重要なことの1つ-しないでくださいこれまでにそのようなものに依存し、neverこれを実際のコードで使用してください!これは興味深い機能であり(常に正しいとは限りません)、機能やそのようなものではありません。そのような「機能」によって生成されたバグを見つけようとしている自分を想像してみてください-悪夢。

ところで-CとC++は、そのような「機能」でいっぱいです。ここに[〜#〜]素晴らしい[〜#〜]スライドショーがあります:- http://www.slideshare.net/olvemaudal/deep-c したがって、より類似した「機能」を表示したい場合は、内部の内容とその機能を理解するだけで、このスライドショーを見るだけで、すぐに表示されます。後悔しているし、経験豊富なc/c ++プログラマーのほとんどでさえ、これから多くを学ぶことができると確信している。

12
cyriel

関数Aでは、変数aは初期化されておらず、その値を出力すると未定義の動作が発生します。

一部のコンパイラでは、a内の変数Aa内のBが同じアドレスにあるため、5、しかし再び、未定義の動作に依存することはできません。

7
Yu Hao

_gcc -Wall filename.c_を使用してコードをコンパイルします。これらの警告が表示されます。

_In function 'B':
11:9: warning: variable 'a' set but not used [-Wunused-but-set-variable]

In function 'A':
6:11: warning: 'a' is used uninitialized in this function [-Wuninitialized]  
_

初期化されていない変数をcに出力すると、未定義の動作が発生します。

セクション6.7.8 C99標準の初期化は言う

自動保存期間を持つオブジェクトが明示的に初期化されていない場合、その値は不確定です。静的ストレージ期間を持つオブジェクトが明示的に初期化されていない場合は、次のようになります。

_— if it has pointer type, it is initialized to a null pointer;
— if it has arithmetic type, it is initialized to (positive or unsigned) zero;
— if it is an aggregate, every member is initialized (recursively) according to these rules;
— if it is a union, the first named member is initialized (recursively) according to these rules.
_

Edit1

As @Jonathon Reinhart _-O_フラグ_gcc-O0_を使用して最適化を無効にすると、出力5が得られる可能性があります。

しかし、これはまったく良い考えではありません。これを運用コードで使用しないでください。

_-Wuninitialized_これは重要な警告の1つですこれを検討する必要がありますデーモンの実行中にクラッシュを引き起こすなど、本番環境に大きなダメージを与えるこの警告を無効にしたりスキップしたりしないでください。


Edit2

ディープC スライドの説明なぜ結果は5 /ガベージなのか。これらのスライドからこの情報を少し修正して追加し、この回答の効果を少し高めます。

ケース1:最適化なし

_$ gcc -O0 file.c && ./a.out  
5
_

おそらく、このコンパイラには、再利用する名前付き変数のプールがあります。たとえば、変数aがB()で使用および解放された場合、A()が整数名aを必要とする場合、変数は同じメモリ位置を取得します。 B()の変数の名前をbに変更すると、_5_が返されるとは思いません。

ケース2:最適化あり

オプティマイザーが起動すると、多くのことが発生する可能性があります。この場合、副作用がないため、B()の呼び出しはスキップできると思います。また、A()main()にインライン化されている場合、つまり、関数呼び出しがない場合でも、驚かないでしょう。 (ただし、A ()にはリンカーの可視性があるため、別のオブジェクトファイルが関数とリンクする場合に備えて、関数のオブジェクトコードを作成する必要があります)。とにかく、コードを最適化すると、出力される値が別の値になると思います。

_gcc -O file.c && ./a.out
1606415608  
_

ごみ!

7
Gangadhar