[C言語]コールスタック(スタックフレーム)の仕組みを復習する

何冊か本を読んだんですが、しっくり来ないのと、
すこし間があくとすぐに忘れてしまうので
自分の中で整理します。

コールスタックとは

コールスタック -wikipedia-より引用

スタックの検査 -gdb-

コールスタック (Call Stack)は、プログラムの実行中にサブルーチンに関する情報を格納するスタックである。
実行中のサブルーチンとは、呼び出されたが処理を完了していないサブルーチンを意味する。
実行スタック (Execution Stack)、制御スタック (Control Stack)、関数スタック (Function Stack)など
とも呼ばれる。
また、単に「スタック」と言ったときにコールスタックを指していることが多い。
コールスタックを正しく保つことは多くのソフトウェアが正常動作するのに重要であり、
その内容は高水準言語にとっても同じである。

つまり、関数の中で関数が呼ばれたときなんかに、
プログラム自体が、次にどこを実行すればいいかわからなくならないように、
きちんと体系的に整理した仕組みで、現在実用されているのがコールスタック
ということで大丈夫でしょうか。

第一段階として、main()関数のみの単純なプログラム

実行すれば

と表示されるだけのシンプルなものです。

MACのアセンブラ長くてめんどくさそうだったのでLinux(ubuntu)で
コンパイルしたものを用意しました。

sample1.c

sample1.s

変数宣言をするだけのプログラムです。それ以外は何もしません。

それをアセンブラにしたソースが上です。

.cfiなんちゃらは無視してOKです。

このmain()関数のスタックフレームは

これによって確保されているようです。
16byte分espが減らされて、ebpから数えてうえから順番に
値(a=10, b=20, c=30)がそれぞれアドレス内に確保されています。

return 0の直前でブレイクさせた状態の情報

Screen Shot 2013-08-31 at 3.59.48

次は、簡単な関数を作成してmain()から呼び出してみます。

アセンブラにしたものがこちら

main()とadd()に対応したアセンブラのコードがそれぞれありますね。

ここがadd()呼び出しの箇所。
引数は後ろからスタックに入れているのがわかります。

この段階では、ソースを見る限りでは

となっています。これがadd()関数呼び出し前の状態(この状態が重要です。)

callでは、関数を呼び出すのと同時に、呼び出された関数が終了した後に、
どこから処理を再開するべきかのアドレス(リターンアドレス)をスタックにプッシュします。
それに伴い、$espも-4されます。

つまりcallされたときのスタックの状態は

となっています。
add関数に入ると、スタックフレームが構築され、関数の処理が実行できるようになります。

最初の数行で実行の準備が行われます。(ラベルは無視してOK)

まずはpush %ebpでebpの値をスタックにプッシュします。

とスタックの状態が変更されます。

その後、movl $esp $ebpでespの値をebpに代入します。

espとebpの値が同じになりました。
最後にsubl $16 %espで、add()関数のためのスタックフレームを16byte確保します。

これで準備OKです。関数内の処理を追っていきます。

ebp+12(つまり引数のb)をeaxに格納し、次にebp+8(つまり引数のa)をedxに格納します

addl $edx, $eaxで二つを足したものを$eaxへ代入。

次のaddl16(%ebp), %eaxで、引数のcと二つを足したものを、更にたして$eaxに代入
(a+b+cを実現しています)

そのあとにebp-4の場所に$eax(a+b+cの答え)を格納してから、ebp-4の値をeaxに格納しています。
(一見無駄な処理ですが、関数のreturnはeaxに格納する決まりなので、そのための慣例的なもの)

この段階のスタックの状態は

最後にleaveとretの処理です。

leaveはスタックフレームの破棄に使われる命令です。内部的にはmov %ebp, %espを
行い、その後、pop %ebpが実行されるのと同義です。

つまり、

この状態にmov $ebp, $espでしてから、pop %ebpします。つまり、add()呼び出し時のebpを
再びebpに戻します。popされたことによって、espは+4ずれ込みます。

これにより

となります。(add()呼び出し時と同じ状態)

最後にretでリターンアドレスに戻ります。pop $eipと同義です。スタックからリターンアドレスを
popして、eip(次に実行される命令のアドレスを格納するレジスタ)に代入して、add()関数が
終了します。

スタックの状態は

となり、add()呼び出し前と全くおなじになっています。
add()関数から、制御が戻ったmain()関数では

returnされた値が格納されているeaxから値を取り出して、printf()に渡していることがわかります。

printfも、同じ流れでスタックに引数が渡されています。

main()関数の終了時もleaveとretを実行し、同じように終了します。
leaveの前の movl $0, %eaxはreturn 0;を実現しています。

こんな感じです。

参考にしていた本に誤植があり、それに気がつくまでに時間がかかってしまった・・・。
でもちゃんと復習できてよかった。


その他参考リンク

X86アセンブラ/GASでの文法

LEAVE命令

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です