次の条件が満たされるなら, 大抵のx86系Cコンパイラのデフォルトでは呼び出し規約は同一のものとなり, ASMも同じコードを利用することができます.
この場合ASMでは次の条件を満たせばよいです. それ以外の複雑な場合はCalling convertionsなどを参照してください. ここでは扱いません.
この条件が満たされたとき, 左から数えてn番目の引数は [esp + 4*n] でアクセスできます. 1番目, 2番目の条件はすでに説明したのでよいでしょう. 3番目はどういう意味でしょうか. たとえばsum()という関数のシンボル名がコンパイラによって異なることがあります. たとえばVC++では_sumとなりますし, High-Cというコンパイラではsumとなります. gccではアンダースコアがついているものとついてないものがあります. VC++で関数を実装しないでビルドすると, しばしば
testsum.obj
testsum.obj : error LNK2001: 外部シンボル "_sum" は未解決です
testsum.exe : fatal error LNK1120: 外部参照 1 が未解決です。
というメッセージが出ます. よく見ると"_sum"の先頭にアンダースコアがついていますが, それのことです.
nasm.cfgの先頭を見てみましょう.
%ifdef WIN32
%define _ADD_UNDERSCORE
%ifdef __BORLANDC__
%define BSS .data
...(snip)
%imacro globaldef 1
%ifdef _ADD_UNDERSCORE
%define %1 _%1
%endif
global %1
%endmacro
NASMの文法についてはここでは触れませんがWIN32が定義されているとき, _ADD_UNDERSCOREというマクロが定義されて操作されているのが分かります. このような仕掛けにより様々なコンパイラに対応できるようにしています. したがってよほど特殊な環境でない限り意識する必要はありません.
func(a1, a2, a3); という関数を呼び出したいなら, [esp + 4*n] に引数を入れていけばよいので
push "a3が入っているレジスタ or メモリ"
push "a2"
push "a1"
call func
add esp, 4 * 3
と逆順にpushします. 注意しないといけないのはpushを3回したのでespは4 * 3減っている点です.
呼び出した後に4 * 3を足さないといけません.
二度続けて呼び出すなら
push "a3が入っているレジスタ or メモリ"
push "a2"
push "a1"
call func
add esp, 4 * 3
push "a3が入っているレジスタ or メモリ"
push "a2"
push "a1"
call func
add esp, 4 * 3
となるわけですが esp の値は相対値が重要なので
push "a3が入っているレジスタ or メモリ"
push "a2"
push "a1"
call func
push "a3が入っているレジスタ or メモリ"
push "a2"
push "a1"
call func
add esp, 4 * 3 * 2
としても構いません. 後者の場合は add esp の操作を一つ減らせます.
その3種類を見てみましょう.
簡単にするためインラインアセンブラを利用します.
また[プロジェクト]->[設定]->[C/C++]->[ファイルリスティング]->[マシン語とソースコード含む]
を選択してコンパイラが出力したアセンブリソース(cod)も見てみましょう.
以下の三つのsumはどれも正しく動作します.
ソースはlinkage.c.
int __cdecl sum_cdecl(int a, int b)
{
__asm {
mov eax, a
add eax, b
}
}
int __fastcall sum_fastcall(int a, int b)
{
__asm {
lea eax, [ecx + edx]
}
}
int __stdcall sum_stdcall(int a, int b)
{
__asm {
mov eax, a
add eax, b
}
}
一つ目のsumが今まで説明してきた呼び出し規約に従ったものです. 二つ目は可能なら引数をレジスタ上でやりとりする規約です. ecxが一つ目の引数, edxが二つ目の引数となっています. 加算は
mov eax, ecx
add eax, edx
とするところですが, lea命令を使うと一つにまとめられます.
三つ目は前のページで少し触れたpascal型の呼び出し規約,
WinAPIの呼び出し規約です. asm文の中は一つ目と同じですが導出されるコードが異なります.
cl linkage.c /O2 /Fa
を実行してみましょう. 生成されたlinkage.asmを順に見てみます.
_a$ = 8
_b$ = 12
_sum_cdecl proc near
mov eax, dword ptr _a$[esp-4]
add eax, dword ptr _b$[esp-4]
ret
_sum_cdecl endp
8[esp - 4] は [esp - 4 + 8] と等価(MASMの文法)なのでこれは今までの説明どおりです.
@sum_fastcall@8 proc near
; _a$ = ecx
; _b$ = edx
lea eax, dword ptr [ecx+edx]
ret
@sum_fastcall@8 endp
関数が呼び出された時点ですでにレジスタに値が代入されています. また関数のシンボル名が@sum_fastcall@8という名前になっていることに注意してください.
_a$ = 8
_b$ = 12
_sum_stdcall@8 proc near
mov eax, dword ptr _a$[esp-4]
add eax, dword ptr _b$[esp-4]
ret 8
_sum_stdcall@8 endp
引数の受取部分は同じですがretではなくret 8になっています.
また関数のシンボル名が_sum_stdcall@8と上記とも異なる名前になっています.
_sum_stdcall@8の最後の8はret 8の意味です.
次に
int a = 3, b = 5;
printf("%d + %d = %d\n", a, b, sum_cdecl(a, b));
printf("%d + %d = %d\n", a, b, sum_fastcall(a, b));
printf("%d + %d = %d\n", a, b, sum_stdcall(a, b));
の部分がどうアセンブルされているかを見ていきます.
_DATA segment
msg DB '%d + %d = %d', 0aH
DB 00H
_DATA ENDS
_TEXT segment
_main proc near
push 5
push 3
call _sum_cdecl
push eax
push 5
push 3
push offset flat:msg
call _printf
add esp, 24
最初の_DATA ...の所はprintfで使われる文字定数の宣言です.
次の_mainの最初の3行でsum_cdecl(3, 5)を実行しています.
返り値はeaxに入っているのでそのまま次のprintfへの引数としてpushされています.
printfを呼び出した後最後にespを復元(4 * 6回push)しています.
call文の度にaddしてもよいのですがまとめて実行されています.
mov edx, 5
mov ecx, 3
call @sum_fastcall@8
push eax
push edx
push ecx
push offset flat:msg
call _printf
add esp, 16
レジスタ渡しのためpushせずに@sum_fastcall@8を呼び出しています. 後半は上記と同じですが, push 5ではなくpush edxとなっています. このほうがコード長が短くてすむからです. 4回pushしたのでadd esp, 16となっています.
push 5
push 3
call _sum_stdcall@8
push eax
push 5
push 3
push offset flat:msg
call _printf
add esp, 16
最後のadd esp, 16以外は一つ目と同じです. 6回pushしているのですが_sum_stdcall@8の中でret 8をしているため24 - 8 = 16 だけaddしています.
今回は引数は二つでしたが三つにするとどうなるか調べてみるとよいでしょう.
__cdeclと__stdcall呼び出し規約では規則的に変化しますが,
__fastcallでは不規則(というか人間にとって覚えにくい)変化をします.
__cdeclと__stdcallとではほとんど同じなのですがC言語の関数呼び出しと同じ
__cdeclを使う方が分かりやすいです.
一番単純なvirtualではない固定引数のメンバ関数については32bitのVC以外, 32/64bit gcc, 64bit VCでは第一引数にthisを渡したCの関数呼び出しと同等です. つまり(動作保証はできませんが)次のコードが動きます.
struct A {
int x_;
int y_;
A() : x_(3), y_(5) {}
int func(int a, int b) const { return x_ + y_ + a + b; }
};
int func2(const A* self, int a, int b)
{
return self->x_ + self->y_ + a + b;
}
int main()
{
int (A::*p)(int, int) const = 0;
*(void**)&p = (void*)func2;
A a;
int x = a.func(123, 4567);
int y = (a.*p)(123, 4567);
...
32bitのVCは特殊でthisはecxで渡され, 呼び出された側でthisを除く引数の個数を開放してやる必要があります.
thisを渡した__stdcallだと一つの変数分余計に開放されてしまうのでうまくいきません. 通常のCで書くのは難しいです.
上記func2相当のものをinlineアセンブラで書くと次のようになります. 下記コードは32/64bit Win/Linux上で動作します.
#if defined(_WIN32) && !defined(_WIN64)
/* int A::func(int, int) */
__declspec(naked) int func2(const A* self, int a, int b)
{
__asm {
mov eax, [ecx] // x_
add eax, [ecx + 4] // y_
add eax, [esp + 4] // a
add eax, [esp + 8] // b
ret 4 * 2 // aとbの分だけpopしてreturn
}
}
#else
int func2(const A* self, int a, int b)
{
return self->x_ + self->y_ + a + b;
}
#endif
int main()
{
int (A::*p)(int, int) const = 0;
*(void**)&p = (void*)func2;
...
#include <stdarg.h>
struct A {
int a_;
A() : a_(100) {}
int sum(int n, ...)
{
int sum = 0;
va_list vl;
va_start(vl, n);
for (int i = 0; i < n; i++) {
int v = va_arg(vl, int);
sum += v;
}
return sum + a_;
}
};
int sum2(const A* self, int n, ...)
{
int sum = 0;
va_list vl;
va_start(vl, n);
for (int i = 0; i < n; i++) {
int v = va_arg(vl, int);
sum += v;
}
return sum + self->a_;
}
int main()
{
A a;
int (A::*p)(int, ...) const = 0;
*(void**)&p = (void*)sum2;
...
そのうち書きたい.