関数呼び出し時に引数および結果がどのように引き渡されているのかをWin32とWin64の両方について確認してみます。Win32ではスタックフレームを使っていたのが、Win64ではレジスタ経由で引渡しが行われているのがわかります。
■Win32
(本記事の初稿は2005年です。64ビットCPUが普及した現在でも有用な内容と思われるので、そのままの内容で公開します。)Win32では、以下の2つのソースファイルを用います。
#include <stdio.h>
unsigned long kasan(unsigned long, unsigned long);
void main(void) {
unsigned long a,b,c;
a=100;
b=100;
c=kasan(a,b);
printf("%I32u + %I32u = %I32u",a,b,c);
}
.386 .model flat,c .code kasan PROC _num01:DWORD, _num02:DWORD mov EBX, _num01 mov EDX, _num02 add EBX, EDX mov EAX, EBX ret kasan ENDP end
ビルドを始める前に、これらのソースファイルのアセンブルリストが出力されるようにします。「ソリューション エクスプローラ」を用いて、win32c.cについては「出力ファイル->アセンブリの出力」を「アセンブリ コード、コンピュータ語コード、ソース コード(/FAcs)」に変更し、win32asm.asmについては、アセンブル時のコマンドライン引数に「/Fl」を追加します。(大文字のエフ、小文字のエルです。)
さっそくビルドし、アセンブルリストを眺めてみます。まずwin32c.cのアセンブルリストです。
.686P
.XMM
.model flat
EXTRN _kasan:PROC
_TEXT SEGMENT
_c$ = -32 ; size = 4
_b$ = -20 ; size = 4
_a$ = -8 ; size = 4
_main PROC ; COMDAT
; 15 : void main(void) {
55 push ebp
8b ec mov ebp, esp
...
; 21 : a=100;
c7 45 f8 64 00 00 00 mov DWORD PTR _a$[ebp], 100 ; 00000064H
; 22 : b=100;
c7 45 ec 64 00 00 00 mov DWORD PTR _b$[ebp], 100 ; 00000064H
; 23 : c=kasan(a,b);
8b 45 ec mov eax, DWORD PTR _b$[ebp] ;<- (1) mov eax, DWORD [ebp-20]
50 push eax ;<- (1)
8b 4d f8 mov ecx, DWORD PTR _a$[ebp] ;<- (2) mov ecx, DWORD [ebp-8]
51 push ecx ;<- (2)
e8 00 00 00 00 call _kasan
83 c4 08 add esp, 8
89 45 e0 mov DWORD PTR _c$[ebp], eax ;<- (3) mov DWORD PTR [ebp-32], eax
...
; 30 : }
8b e5 mov esp, ebp
5d pop ebp
c3 ret 0
_main ENDP
_TEXT ENDS
END
(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のアセンブルリストはこうなります。
.386
.model flat,c
.code
kasan PROC _num01:DWORD, _num02:DWORD ; kasan(a,b)
; push ebp 暗黙の了解で、左記命令が実行される
; mov ebp, esp 暗黙の了解で、左記命令が実行される
8B 5D 08 mov EBX, _num01 ; mov EBX, DWORD PTR [EBP+08h] 呼び出し側でスタックに積んだ変数aの値
8B 55 0C mov EDX, _num02 ; mov EDX, DWORD PTR [EBP+0Ch] 呼び出し側でスタックに積んだ変数bの値
03 DA add EBX, EDX
8B C3 mov EAX, EBX
; mov esp, ebp 暗黙の了解で、左記命令が実行される
; pop ebp 暗黙の了解で、左記命令が実行される
C3 ret
kasan ENDP
end
呼び出し側でスタックに積んだ引数が正しく呼び出され、計算結果がEAXに格納されているのがわかります。
■Win64
Win64では、以下の2つのソースファイルを用います。
#include <stdio.h>
unsigned _int64 kasan(unsigned _int64,unsigned _int64,
unsigned _int64,unsigned _int64,
unsigned _int64,unsigned _int64);
void main(void) {
unsigned _int64 i0,i1,i2,i3,i4,i5;
unsigned _int64 sum;
i0=100;
i1=100;
i2=100;
i3=100;
i4=100;
i5=100;
sum=kasan(i0,i1,i2,i3,i4,i5);
printf("%I64u + .. + %I64u = %I64u",i0,i5,sum);
}
.code kasan PROC mov RBX, RCX add RBX, RDX add RBX, R8 add RBX, R9 add RBX, QWORD PTR [rsp+40] add RBX, QWORD PTR [rsp+48] mov RAX, RBX ret kasan ENDP end
一見するとわかるように、アセンブリ言語のソースには、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のままになっていますが、これは書き換えの手間を省くためです。この点についてはご了承ください。
EXTRN kasan:PROC
_TEXT SEGMENT
i1$ = 48
i5$ = 56
i0$ = 64
i3$ = 72
sum$ = 80
i4$ = 88
i2$ = 96
main PROC
; 15 : void main(void) {
48 83 ec 78 sub rsp, 120 ; 00000078H
; 21 : i0=100;
48 c7 44 24 40 64 00 00 00 mov QWORD PTR i0$[rsp], 100 ; 00000064H <- (1)
; 22 : i1=100;
48 c7 44 24 30 64 00 00 00 mov QWORD PTR i1$[rsp], 100 ; 00000064H <- (2)
; 23 : i2=100;
48 c7 44 24 60 64 00 00 00 mov QWORD PTR i2$[rsp], 100 ; 00000064H <- (3)
; 24 : i3=100;
48 c7 44 24 48 64 00 00 00 mov QWORD PTR i3$[rsp], 100 ; 00000064H <- (4)
; 25 : i4=100;
48 c7 44 24 58 64 00 00 00 mov QWORD PTR i4$[rsp], 100 ; 00000064H <- (5)
; 26 : i5=100;
48 c7 44 24 38 64 00 00 00 mov QWORD PTR i5$[rsp], 100 ; 00000064H <- (6)
; 27 : sum=kasan(i0,i1,i2,i3,i4,i5);
48 8b 44 24 38 mov rax, QWORD PTR i5$[rsp]
48 89 44 24 28 mov QWORD PTR [rsp+40], rax ;<- (7) 6番目の引数
48 8b 44 24 58 mov rax, QWORD PTR i4$[rsp]
48 89 44 24 20 mov QWORD PTR [rsp+32], rax ;<- (8) 5番目の引数
4c 8b 4c 24 48 mov r9, QWORD PTR i3$[rsp] ;<- (9) 4番目の引数
4c 8b 44 24 60 mov r8, QWORD PTR i2$[rsp] ;<- (10) 3番目の引数
48 8b 54 24 30 mov rdx, QWORD PTR i1$[rsp] ;<- (11) 2番目の引数
48 8b 4c 24 40 mov rcx, QWORD PTR i0$[rsp] ;<- (12) 1番目の引数
e8 00 00 00 00 call kasan
48 89 44 24 50 mov QWORD PTR sum$[rsp], rax ;<- (13)
; 30 : }
33 c0 xor eax, eax
48 83 c4 78 add rsp, 120 ; 00000078H
c3 ret 0
main ENDP
_TEXT ENDS
END
(1)から(6)で変数i0~i5までの初期化を行っています。どれも即値をmovしているだけなので問題ないです。関数kasan()呼び出しに備えて(7)から(12)までで引数を準備していますが、(9)から(12)、すなわち引数の内(左から数えて)4個までは、スタックフレームに積まずにレジスタ経由で引き渡そうとしているところがWin32と違っています。残り2個の引数は(7)と(8)でスタックフレームに積んでいます。
関数kasan()呼び出しの結果は(13)で変数sumに代入していることがわかります。すなわち、計算結果がRAXレジスタ経由で返されていることがわかります。
.code
kasan PROC
48/ 8B D9 mov RBX, RCX ; 1番目の引数
48/ 03 DA add RBX, RDX ; 2番目の引数
49/ 03 D8 add RBX, R8 ; 3番目の引数
49/ 03 D9 add RBX, R9 ; 4番目の引数
48/ 03 5C 24 28 add RBX, QWORD PTR [rsp+40] ; 5番目の引数
48/ 03 5C 24 30 add RBX, QWORD PTR [rsp+48] ; 6番目の引数
48/ 8B C3 mov RAX, RBX ; 計算結果を呼び出し側に返すために代入
C3 ret
kasan ENDP
end
関数kasan()そのもののアセンブルリストです。全部の引数をRBXに加算し、その結果をRAXに代入することで呼び出し側に返します。引数を取得する時、4個までだったらレジスタから取ってくればよいので、順序さえ間違えなければ特に悩むことはありませんが、5個以上になるとスタックフレームから取ってくることになるので、注意する必要があります。
5番目の引数は、以下のようにしてスタックフレームに積みました。
48 8b 44 24 58 mov rax, QWORD PTR i4$[rsp] 48 89 44 24 20 mov QWORD PTR [rsp+32], rax ;<- (1) 5番目の引数をスタックフレームへ ... e8 00 00 00 00 call kasan ;<- (2) 48 89 44 24 50 mov QWORD PTR sum$[rsp], rax ;<- (3)
呼び出し側(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日(日)