Cとの連係 その2

前回は主に呼び出される側の話でしたが, 今回は呼び出す側についても触れます. その後VC++での呼び出し規約についてもう少し詳しく説明します.

呼び出される側
呼び出される側について整理します.

次の条件が満たされるなら, 大抵のx86系Cコンパイラのデフォルトでは呼び出し規約は同一のものとなり, ASMも同じコードを利用することができます.

大抵のコンパイラ
VC++, Borland C++, gcc, cygwin, mingw, icc, High-Cなどで大丈夫でした. うまくいかないコンパイラを私は知りません.

この場合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というマクロが定義されて操作されているのが分かります. このような仕掛けにより様々なコンパイラに対応できるようにしています. したがってよほど特殊な環境でない限り意識する必要はありません.

呼び出す側
上記条件を満たしていればCから呼び出すときは何も考えなくてもよいのですが, ASMから呼び出す場合はどうすればよいのでしょうか.

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 の操作を一つ減らせます.

VC++では
32bit VC++には呼び出す側は3種類(C++を含めると4種類)の呼び出し方法があります. 64bit Windows環境では__cdecl, __fastcall, __stdcall全て同じ呼び出し方法に統一されました. 詳細はx64アセンブリ言語プログラミング参照.

その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を使う方が分かりやすいです.

.name(C++)
C++では
C++のメンバ関数の呼び出し方法について見てみます.
virtualではない固定数引数のメンバ関数

一番単純な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;
    ...
virtualではない可変引数のメンバ関数
この場合, 特殊だった32bit VCも上記と同じく先頭にthisを渡したのと同じ方式になります. したがって下記コードは32/64bit VC/gccの全ての環境で動作します(もちろん規格上の保証は無いですが).
#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;
    ...
virtualなメンバ関数

そのうち書きたい.


御意見は光成滋生<herumi@nifty.com>までお願いします