スタンフォードのチュートリアルとGCCの間の競合


82

この映画(約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

41
「彼らは同じスペースをうまく使用している」-それは正しくない。彼らはそうかもしれません。またはそうでないかもしれません。そして、あなたはどちらの方法でもこれに頼ることはできません。
マット

17
これを演習としてどのように使用するのだろうか。本番コードでこれを使用すると、撃たれるだろう。
AndersK 2013年

12
@claptrapたぶん、コールスタックがどのように機能するかを学び、コンピューターが内部で何をしているのかを理解するためですか?人々はこの方法を真剣に受け止めています。
Jonathon Reinhart 2013年

9
@claptrap繰り返しますが、これは学習演習です。アセンブリレベルで何が起こっているのかを理解していれば、「ジャンプしなければならないフープ」はすべて理にかなっています。OPが「実際の」プログラムでこのようなものを使用する意図があるかどうかは真剣に疑っています(使用する場合は、キックする必要があります)
Jonathon Reinhart 2013年

12
この例は、2つのローカル変数の名前が同じであるため、疑うことを知らない人を誤解させます。しかし、これは何が起こっているかとは無関係です。変数の数とタイプだけが重要です。異なる名前はまったく同じように機能するはずです。
alexis 2013年

回答:


130

はい、はい、これは未定義の動作あなたが初期化されていない変数を使用しているので、1

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

の内部でint a使用されていないためvoid B()、コンパイラはそのコードを最適化し、スタック上のその場所に5が書き込まれることはなかったと思います。追加してみてくださいprintfB()にも-それだけで動作する可能性があります。

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

編集:私はあなたのコードをgcc -O0(64ビット)でコンパイルしました、そして実際、プログラムはコールスタックに精通している人が期待するように5を出力します。実際、それはなくても機能しました-O0。32ビットビルドは動作が異なる場合があります。

免責事項:Doが今までになく、これまでの「本物」のコードにこのようなものを使用!

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]。したがって、この場合、ローカル変数同じ場所に配置されませんでした。追加することによりprintf()、呼をA、同様のスタックフレームを引き起こしますAB同一である、および印刷します55


7
良い免責事項!
トビアスWärre

5
一度は機能したとしても、一部のアーキテクチャでは信頼性がありません。割り込みは、いつでもスタックポインタの下にあるすべてのものを吹き飛ばします。
マーティンジェームス

6
「未定義の振る舞い」についてさえ言及していない答えに非常に多くの賛成票があります。その上、それも受け入れられます。
BЈовић

25
また、実際に質問に答えるので受け入れられます。
slebetman 2013年

8
@BЈовићあなたはビデオを見ましたか?ほら、みんなと彼らの兄弟は、あなたが実際のコードでこれをするべきではないことを知っています、そしてそれは未定義の振る舞いを引き起こします。それは重要ではありません。重要なのは、コンピューターは明確に定義された予測可能なマシンであるということです。x86ボックス(およびおそらく他のほとんどのアーキテクチャ)では、正常なコンパイラーと、場合によってはコード/フラグのマッサージがあれば、これ期待どおりに機能します。このコードとビデオは、コールスタックがどのように機能するかを示すためのものにすぎません。それがひどく気になるなら、私はあなたが他の場所に行くことを提案します。私たちの好奇心旺盛なタイプの中には、物事を理解するのが好きな人もいます。
Jonathon Reinhart 2013年

36

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


6
より正確には、アドレスが取得されない単一化変数を使用することは、未定義の動作です。
Jens Gustedt 2013年

@JensGustedtいいコメント。blog.frama-c.com/index.php?post/2013/03/13/…の「次の例」セクションについて何か言いたいことがありますか?
パスカルキュオック2013年

@PascalCuoq、これは標準化委員会で進行中の議論でさえあるようです。初期化されているかどうかわからない場合でも、ポインタを介して取得したメモリを検査することが理にかなっている場合があります。すべての場合に単純に未定義にすることは、制限が厳しすぎます。
Jens Gustedt 2013年

@JensGustedt:アドレスを取得すると、どのように使用すると動作が定義されますか:{ int uninit; &uninit; printf("%d\n", uninit); }まだ未定義の動作があります。一方、任意のオブジェクトをunsigned char;の配列として扱うことができます。それはあなたが考えていたものですか?
キーストンプソン

@KeithThompson、いいえ、その逆です。そのアドレスが取られることはありませんように変数を持つ、それはUBにリードを初期化されていません。不確定な値を読み取ること自体は未定義の動作ではなく、内容は予測不可能です。6.3.2.1 p2以降:左辺値が、レジスタストレージクラスで宣言された可能性のある自動ストレージ期間のオブジェクトを指定し(アドレスが取得されたことがない)、そのオブジェクトが初期化されていない(イニシャライザで宣言されておらず、割り当てられていない)場合使用前に実行されている)、動作は定義されていません。
Jens Gustedt 2013年

12

-覚えておくべき重要な点はありません、これまでそのような何かに依存しておらず、決して実際のコードでこれを使用します!それはただ面白いことであり(常に真実であるとは限りません)、機能などではありません。そのような「機能」によって生成されたバグ、つまり悪夢を見つけようとしていると想像してみてください。

ところで。--CとC ++にはそのような「機能」がたくさんありますこれは、すばらしいスライドショーです。http//www.slideshare.net/olvemaudal/deep-cしたがって、より類似した「機能」を確認したい場合は、その内容を理解してください。内部とその仕組みについては、このスライドショーをご覧ください。後悔することはなく、経験豊富なc / c ++プログラマーのほとんどでもこれから多くのことを学ぶことができると確信しています。


7

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

一部のコンパイラでは、ainAainの変数がB同じアドレスにあるため5、出力される場合がありますが、未定義の動作に依存することはできません。


1
チュートリアルは100%正しいですが、元のポスターs machine will be the same depends on the assembly generated by the compiler. As @JonathonReinhart pointed out the call to B() `の結果が最適化されているかどうか。
ロイドクローリー

1
「そのチュートリアルが間違っている」という言い回しに問題があります。実際にチュートリアルを見に行きましたか?これは、このようなクレイジーなことを行う方法を教えることではなく、コールスタックがどのように機能するかを示すことを目的としています。その場合、チュートリアルは完全に正しいです。
Jonathon Reinhart 2013年

@JonathonReinhartチュートリアルを見ていませんでした。この例はチュートリアルからのものだと思ったので、この部分を削除します。
Yu Hao

@LloydCrawleyチュートリアルに関する部分を削除しました。私はそれがスタックアーキテクチャについてであることを知っています5、それはそれが印刷したときにそれらが同じアドレスにあることを意味しました、しかし明らかにJonathonReinhartははるかに良い説明をしています。
Yu Hao

7

でコードをコンパイルします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]  

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

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

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

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.

編集1

@JonathonReinhartとして-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  

ガービッジ!


1
編集2、ケース1のロジックは完全に正しくありません。それはまったくそれがどのように機能するかではありません。ローカル変数の名前はまったく意味がありません。
Jonathon Reinhart 2013年

@JonathonReinhart回答で述べたように、これをdeepcスライドから追加しましたが、どの根拠に基づいて間違っているかを説明してください。
Gangadhar 2013年

3
スタックスペースと変数名の間には何の関連もありません。この例は、概念的には、2番目の関数呼び出しのスタックフレームが2番目の関数呼び出しのスタックフレームを単純にオーバーレイするという事実に依存しています。名前が何であるかは関係ありません。両方のメソッドシグネチャが同じである限り、同じことが起こる可能性があります。他の人が指摘しているように、それが組み込みシステムにあり、A()とB()の呼び出しの間にハードウェア割り込みが処理された場合、スタックにはランダムな値が含まれます。Code Guard for Borlandのような古いツールでは、各呼び出しの前にスタックにゼロを書き込むことができました。
ダン・ヘインズ

@DanHaynesあなたのコメントは、変数タイプと関数プロトタイプが同じである限り、2番目の関数呼び出しのスタックフレームが最初の関数呼び出しのスタックフレームをオーバーレイする可能性があることを納得させます。はい、変数名とは何の関係もないことに同意します。
ガンガダール2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.