スタックフレーム - 関数に渡される引数を知る

この資料は x86 アーキテクチャに関する資料です。

障害の原因究明をする際に、ある関数へ渡されるデータがわかると都合が良い場合が少なくありません。

つまり、Func1(int x) というような関数に x = 10 というときにエラーが発生する、という情報がわかれば開発者がデバッグするために非常に有効な情報といえるでしょう。 トラブルシューティングを行うときに、引数の受け渡しがどのように行われるのかを知ることは非常に重要です。

x86 インテルアーキテクチャではプロシージャを呼び出す前にスタックに引数を積み上げます。 スタックは主にここで説明するような関数呼び出しを行うために利用される、連続したメモリ領域のことです。 スタックは、アドレスが小さいほうへとデータが積み上げられます。スタックへデータを積み上げることを PUSH といい、 逆にスタックの上部からデータを取り出すことを POP といいます。 図のようにある時点のスタックの先頭は ESP レジスタに記録されます。 ですから、スタックにデータを PUSH と同時に ESP の値は小さくなります (アドレスが小さいほうへ伸びることに注意してください)。 そして、POP と同時に ESP レジスタの値は大きくなります。

プロシージャを呼び出す際にスタックが利用されます。引数がスタックに積み上げられるのです。

では、ここで実際のアプリケーションの動作を見てみましょう。テストコードは次のような単純なものです。 Win32 アプリケーションの開始地点である WinMain 関数から単に MessageBox を呼び出すだけです。 引数は NULL (実際には 0 です), "Hello World" (文字列), "Message" (文字列) そして MB_OK|MB_ICONINFORMATION (実際には 0x00000040 という値に定義されます) の 4 つです。

int APIENTRY WinMain(   HINSTANCE hInstance, HINSTANCE hPrevInstance,
  LPSTR lpCmdLine, int nCmdShow )
{
  ::MessageBox(NULL, TEXT("Hello World."), TEXT("Message"), MB_OK|MB_ICONINFORMATION);

  return 0;
}

これをコンパイルして実行すると次のようなメッセージボックスが表示されます。

コンパイルしたバイナリを逆アセンブルし、MessageBox 関数を呼び出す箇所を見てみましょう。それは次のようになります。

0:000> u Call01!WinMain
*** WARNING: Unable to verify checksum for Call01.exe
Call01!WinMain:
00401000 6a40             push    0x40
00401002 6840704000       push    0x407040
00401007 6830704000       push    0x407030
0040100c 6a00             push    0x0
0040100e ff15a0604000    call dword ptr [Call01!_imp__MessageBoxA (004060a0)]
00401014 33c0             xor     eax,eax
00401016 c21000           ret     0x10
00401019 90               nop
0:000> da 0x407040
00407040  "Message"
0:000> da 0x407030
00407030  "Hello World."

ここでデバッガの da コマンド (後の章で解説します) でメモリの内容を見ると、アドレス 0x407040 は文字列 "Message" であり、0x407030 は "Hello World" のことであることがわかります。

結局 アドレス 00401000 から 0040100c までのところに見える PUSH 4つがスタックに引数をプッシュしているところです。 4 つの引数を PUSH する順番は上から順に 0x40, "Message", "Hello World" そして 0 ですから、 これはちょうどソースコード上の引数を右から左に向かって順番に PUSH したことになります。

尚、 ソースコード上は MessageBox ですが、逆アセンブルで IAT (Import Address Table) に見えているのは MessageBoxA です。 明示的に Unicode を使うよう指定されていない場合、Visual C++ 6.0 ではこのように MessageBox は MessageBoxA と解釈されます。 さらに MessageBoxA はその内部で MessageBoxExA を呼び出します。

関数を呼び出す直前、つまり引数を 4 つ PUSH した時点でのスタックの様子を図にすると次のようになります。

では先に進みましょう。MessageBox (ここでは MessageBoxExA Note 参照) 内部では引数を次のように扱っています。

USER32!MessageBoxExA:
77d1adfe 55               push    ebp
77d1adff 8bec             mov     ebp,esp
77d1ae01 6aff             push    0xff
77d1ae03 ff7518           push    dword ptr [ebp+0x18]
77d1ae06 ff7514           push    dword ptr [ebp+0x14]
77d1ae09 ff7510           push    dword ptr [ebp+0x10]
77d1ae0c ff750c           push    dword ptr [ebp+0xc]
77d1ae0f ff7508           push    dword ptr [ebp+0x8]
77d1ae12 e804000000       call    USER32!MessageBoxTimeoutA (77d1ae1b)
77d1ae17 5d               pop     ebp
77d1ae18 c21400           ret     0x14

Note (MessageBoxA から MessageBoxExA への道のり) で触れたように MessageBoxA から MessageBoxExA を呼び出す途中で、0 がひとつ PUSH されています。 従いまして、 77d1ae03 から dword ptr [<ebp からの相対アドレス>] で指し示される値は次の図から明らかになります。

関数内部では通常、関数の呼び出し元から渡される引数は EBP からの正 (プラス) の方向のオフセットで表されます。それは今見てきましたように引数がスタックを経由して関数に渡され、かつスタックがアドレスの小さいほうへ伸びるという性質を持っているからです。 同じように確かめることが可能ですが、関数内の変数は EBP から負 (マイナス) 方向のオフセットとして表されます。

つまり、 「ローカル変数は EBP からのマイナスのオフセット」、 「引数は EBP からプラスのオフセット」 にあるます。

関数の実行が終わり呼び出し元へ戻るときには RET 命令を実行します。これによって伸びたスタックを巻き戻します。

以上のように関数に入る前にスタックに引数が PUSH され、関数内部ではさらに EBP を保存するために PUSH し、関数内部で使用する自動変数がある場合にはさらにスタックに自動変数の領域を確保します。 この一連の流れはほとんど決まりきっています。このシーケンスはエピローグコードと呼ばれています。 そして関数から戻る前に保存していたレジスタの情報を POP し RET によってスタックを巻き戻します。このシーケンスは プロローグコード と呼びます。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説