@ウィキモバイル > PC版はこちら
@wiki検索

組み込み Linux 開発の手引き

トップ | メニュー | ページ一覧 | タグ一覧 | ウィキ内検索
▼下へ[8]

スタックの使い方


main関数のスタックの使い方


C言語でのプログラミングに関する情報は、ネット上で数多く議論されているようですが、C言語で書かれたプログラムがgccによって、どのようなアセンブラ言語にコンパイルされるのか?ということについては、情報が少ないと思います。
スタックの使われ方に注目しながら、まとめてみたいと思います。

次のような単純なC言語のプログラムがあるとします。
#include <stdio.h>

int add(int arg1, int arg2)
{
        return arg1 + arg2;
}

int main(int argc, char *argv[])
{
        int a;
        int b;
        int ret;

        a = 3;
        b = 5;

        ret = add(a, b);

        return ret;
}

このプログラムをgccでコンパイルし、
$ gcc -o main main.c
逆アセンブルしてみると、次のようになりました。
$ objdump -d main
・・・
08048344 <add>:
 8048344:       55                      push   %ebp
 8048345:       89 e5                   mov    %esp,%ebp
 8048347:       8b 45 0c                mov    0xc(%ebp),%eax
 804834a:       03 45 08                add    0x8(%ebp),%eax
 804834d:       5d                      pop    %ebp
 804834e:       c3                      ret    

0804834f <main>:
 804834f:       8d 4c 24 04             lea    0x4(%esp),%ecx
 8048353:       83 e4 f0                and    $0xfffffff0,%esp
 8048356:       ff 71 fc                pushl  -0x4(%ecx)
 8048359:       55                      push   %ebp
 804835a:       89 e5                   mov    %esp,%ebp
 804835c:       51                      push   %ecx
 804835d:       83 ec 18                sub    $0x18,%esp
 8048360:       c7 45 f0 03 00 00 00    movl   $0x3,-0x10(%ebp)
 8048367:       c7 45 f4 05 00 00 00    movl   $0x5,-0xc(%ebp)
 804836e:       8b 45 f4                mov    -0xc(%ebp),%eax
 8048371:       89 44 24 04             mov    %eax,0x4(%esp)
 8048375:       8b 45 f0                mov    -0x10(%ebp),%eax
 8048378:       89 04 24                mov    %eax,(%esp)
 804837b:       e8 c4 ff ff ff          call   8048344 <add>
 8048380:       89 45 f8                mov    %eax,-0x8(%ebp)
 8048383:       8b 45 f8                mov    -0x8(%ebp),%eax
 8048386:       83 c4 18                add    $0x18,%esp
 8048389:       59                      pop    %ecx
 804838a:       5d                      pop    %ebp
 804838b:       8d 61 fc                lea    -0x4(%ecx),%esp
 804838e:       c3                      ret    
 804838f:       90                      nop
・・・
単純なCプログラムが、何故このような難解なアセンブラにコンパイルされてしまうのでしょうか?少しずつ紐解いてみましょう。

main関数が呼び出される前

main関数が呼び出される直前のスタックは次のような状態になっています。

スタックには、呼び出し元へのリターンアドレス(呼び出し元へ戻るために必要)とmain関数に渡された引数がスタックにのっかっている状態となっています。
この図には書いてありませんが、argcの下には、argvの情報が格納されています。

プロローグフェーズ

gccでコンパイルした場合、必ずプロローグというフェーズがあります。このフェーズでは、スタックを利用する準備をします。主に、各種レジスタの保存や設定をおこないます。
main関数の中の下記の6行が、このプロローグフェーズに該当します。
804834f:       8d 4c 24 04             lea    0x4(%esp),%ecx   ※ ECXレジスタが呼び出し元が用意したスタックを指すようにする。
8048353:       83 e4 f0                and    $0xfffffff0,%esp ※ スタックを16バイト境界に揃える(注:何故これが必要なのか分からない)
8048356:       ff 71 fc                pushl  -0x4(%ecx)       ※ リターンアドレスをコピーする(注:何故これが必要なのか分からない。プログラム動作上、これは無くてもいいはず)
8048359:       55                      push   %ebp             ※ EBPレジスタに入っていた値を保存する
804835a:       89 e5                   mov    %esp,%ebp        ※ EBPレジスタに現在のESP(スタックポインタ)の値をセットする
804835c:       51                      push   %ecx             ※ ECXレジスタを保存する
上記の命令を実行した後の、スタックの状態は次のようになります。

空き領域ができてしまっていますが、これは、スタックを16バイト境界に揃えたためです。なぜgccは16バイト境界にスタックを揃えているのか現在のところ分かりません。

ローカル変数の確保フェーズ

main関数では、a、b、そしてretというローカル変数を使用していますので、そのための領域をスタックに確保しなければいけません。
804835d:       83 ec 18                sub    $0x18,%esp       ※ローカル変数の確保
8048360:       c7 45 f0 03 00 00 00    movl   $0x3,-0x10(%ebp) ※ 変数aに3を代入
8048367:       c7 45 f4 05 00 00 00    movl   $0x5,-0xc(%ebp)  ※ 変数bに5を代入
この3行がローカル変数の確保フェーズ&初期化フェーズに該当します。
スタックの状態は次のようになります。

add関数の呼び出し

add関数を呼び出す為の準備および実際に呼び出します。下記5行が該当します。
804836e:       8b 45 f4                mov    -0xc(%ebp),%eax
8048371:       89 44 24 04             mov    %eax,0x4(%esp)
8048375:       8b 45 f0                mov    -0x10(%ebp),%eax
8048378:       89 04 24                mov    %eax,(%esp)
804837b:       e8 c4 ff ff ff          call   8048344 <add>
call命令を呼び出すと自動的に、スタックにリターンアドレスが積まれるので、この段階でのスタックの状態は次のようになります。

使われていない空き領域が発生しているのが分かると思います。これは、ローカル変数の確保フェーズで領域を確保しすぎたためです。なぜ0x18バイト確保したのでしょうか?0x14バイトを確保したほうがきっちりと必要な分だけスタックを使えるのでメモリの節約になると思うのですが・・。謎です。ちなみに、コンパイル時に「-O2」という最適化オプションをつけたところ、このような無駄な空き領域はなくなりましたし、他にも凄いメモリ節約をしたアセンブラを生成してくれました。興味のある方は試してみてください(^-^)。

add関数から戻る

add関数から戻ってきたら、結果をスタックに確保してあるretの領域に保存します。
また、return命令で値を呼び出し元に返すために、EAXレジスタに保存しておきます。関数は返り値をEAXレジスタを介して呼び出し元に返します。
8048383:       8b 45 f8                mov    -0x8(%ebp),%eax

ローカル変数を解放する。

確保しておいたローカル変数領域は、呼び出し元に制御を移すまえに完全に消去しておきます。ローカル変数は、定義した関数内でのみ有効ということが良くわかるでしょう。
8048386:       83 c4 18                add    $0x18,%esp

エピローグフェーズ

呼び出し元に戻る前に保存しておいたレジスタを復帰し、その後、ret命令で呼び出し元の関数へ戻ります.
8048389:       59                      pop    %ecx            ※ ECXレジスタの復帰
804838a:       5d                      pop    %ebp            ※ EBPレジスタの復帰
804838b:       8d 61 fc                lea    -0x4(%ecx),%esp ※ ESP(スタックポインタ)の復帰
804838e:       c3                      ret                    ※ 呼び出し元へ戻る

このページを書いていて思ったこと

gccは、-O2などの最適化オプションをつけなくては、めちゃくちゃ馬鹿みたいですね。プログラムを生成するときには、できるだけ-O2オプションをつけておいた方がよさそうです。

上へ[2]


トップページ
ページ一覧
ウィキ内検索
ホットワード
タグ一覧
ページの先頭へ[2]
ページの下へ[8]
友達に教える
PC版はこちら








関連語句: エピローグ アセンブラ言語 ローカル変数

PCで@ウィキに新規登録


直前のページはの戻るボタンで


@ウィキモバイル [0]