Win32とWin64でのFunction call

関数呼び出し時に引数および結果がどのように引き渡されているのかを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日(日)

スポンサーリンク