Win32とWin64でのFunction call

関数呼び出し時に引数および結果がどのように引き渡されているのかをWin32とWin64の両方について確認してみます。Win32ではスタックフレームを使っていたのが、Win64ではレジスタ経由で引渡しが行われているのがわかります。

■Win32

(本記事の初稿は2005年です。64ビットCPUが普及した現在でも有用な内容と思われるので、そのままの内容で公開します。)Win32では、以下の2つのソースファイルを用います。

ビルドを始める前に、これらのソースファイルのアセンブルリストが出力されるようにします。「ソリューション エクスプローラ」を用いて、win32c.cについては「出力ファイル->アセンブリの出力」を「アセンブリ コード、コンピュータ語コード、ソース コード(/FAcs)」に変更し、win32asm.asmについては、アセンブル時のコマンドライン引数に「/Fl」を追加します。(大文字のエフ、小文字のエルです。)

さっそくビルドし、アセンブルリストを眺めてみます。まずwin32c.cのアセンブルリストです。

(1)の"mov eax, DWORD PTR _b$[ebp]"は"mov eax, DWORD PTR [ebp-20]"と解釈されます。左側に並んでいる3バイトのマシン語コードは、1バイト目がオペコード、2バイト目がmodR/Mバイト、3バイト目がディスプレースメントになります。(ディスプレースメントは符号有の数として扱われ、この場合は[ebp-20]とebpから負の変位になる。)

(1)で変数bの値をスタックに積み、(2)で変数aの値をスタックに積んでから、kasan()を呼び出しているので、引数をスタック経由で引き渡していることがわかります。さらに(3)でその計算結果を変数cに格納しているところから、計算結果がEAXレジスタ経由で引き渡されていることがわかります。

関数kasan()を記述しているwin32asm.asmのアセンブルリストはこうなります。

呼び出し側でスタックに積んだ引数が正しく呼び出され、計算結果がEAXに格納されているのがわかります。

■Win64

Win64では、以下の2つのソースファイルを用います。

一見するとわかるように、アセンブリ言語のソースには、16ビット時代から32ビット時代までおなじみだった「.386」や「.mode flat,c」などの擬似命令がありません。プラットフォームWin64(AMD64)用にビルドし、アセンブルリストを眺めます。(ビルドする前に、Win32時と同様にしてアセンブルリストが出力されるようにしてください。)

※注意※ 上記アセンブリ言語ソースコード(win64asm.asm)ではRBXを使っていますが、Win64(x64)のコンパイラではRBXレジスタは「Must be preserved by called function」(レジスタを使うときは、呼び出された側で値を保存しておくこと)になっています。よって、実際のプログラミングではRBX以外のレジスタ(R10, R11など)を使うか、PROCの冒頭でPUSH RBXして、ret直前でPOP RBXした方が安全であるものと思われます。本ページの記載はRBXのままになっていますが、これは書き換えの手間を省くためです。この点についてはご了承ください。

(1)から(6)で変数i0~i5までの初期化を行っています。どれも即値をmovしているだけなので問題ないです。関数kasan()呼び出しに備えて(7)から(12)までで引数を準備していますが、(9)から(12)、すなわち引数の内(左から数えて)4個までは、スタックフレームに積まずにレジスタ経由で引き渡そうとしているところがWin32と違っています。残り2個の引数は(7)と(8)でスタックフレームに積んでいます。

関数kasan()呼び出しの結果は(13)で変数sumに代入していることがわかります。すなわち、計算結果がRAXレジスタ経由で返されていることがわかります。

関数kasan()そのもののアセンブルリストです。全部の引数をRBXに加算し、その結果をRAXに代入することで呼び出し側に返します。引数を取得する時、4個までだったらレジスタから取ってくればよいので、順序さえ間違えなければ特に悩むことはありませんが、5個以上になるとスタックフレームから取ってくることになるので、注意する必要があります。

5番目の引数は、以下のようにしてスタックフレームに積みました。

呼び出し側(win64c.c)において(1)のQWORD PTR [rsp+32]でアクセスした変数を呼び出された側(win64asm.asm)でアクセスするときのディスプレースメントを求めるには、(2)のcall命令で戻り番地を何バイトでスタックに積むのかを考える必要があります。(2)はオペコードがE8hで4バイトの(call先への)コード・オフセットを伴っていますので、32ビット長のディスプレースメントを使うnear callであることがわかります。

32ビット長のディスプレースメントを使うnear callはAMD64/EM64Tの64-Bit Modeでは、ディスプレースメントが符号付で64ビットに拡張された上でRIPに加算されて飛んでいき、call時には戻り番地((3)の命令への番地)が64ビット(=8バイト)でスタックに積まれます。すなわち、呼び出し側(win64c.c)でQWORD PTR [rsp+32]でアクセスした変数は、32+8=40ですから、呼び出された側(win64asm.asm)ではQWORD PTR [rsp+40]とすればアクセスできることになります。

起草日:2005年1月3日(月)(www.marbacka.net内の別のサイトで公開)
最終更新日:2017年2月19日(日)

スポンサーリンク