サブルーチンの初歩

実際にアセンブリ言語で簡単なコードを記述し, C言語から呼び出してみます.



まずは足し算

とりあえず、足し算するプログラムを書いてみます. NASM + VC++用サンプルを参考にしてください.

#include <stdio.h>
/**
    @return a + b を返す.
*/
int sum(int a, int b);

int main(int argc, char *argv[])
{
    int a, b;
    a = atoi(argv[1]);
    b = atoi(argv[2]);
    printf("%d + %d = %d\n", a, b, sum(a, b));
    return 0;
}

sum()はint型の引数を二つ取り, その値を足したものを返す関数です. これをASMで作成します.

NASM形式の場合
%include "nasm.cfg"

    segment_text
proc    sum
    mov    eax, [esp+4]
    add    eax, [esp+8]
    ret

    end

%include "nasm.cfg"
NASM(今回使うアセンブラ)はさまざまなOS/環境/フォーマットで 使用できるようになっているので設定がややこしいです. 私は使いやすいようにnasm.cfgにマクロを定義して読み込んでいます.
このファイルは午後のこ~だでも使われています.
VC++で外部アセンブラとして使うためにはいくつか設定が必要です. [設定]->[カスタムビルド]で[コマンド]に

nasmw -DWIN32 -f win32 -i$(InputDir)\ $(InputPath) -o $(OutDir)/$(InputName).obj

[出力]に

$(OutDir)/$(InputName).obj

を書いてください(ディレクトリ構成によって随時変更すること).

Linuxの場合はMakefileに

.asm.o:
    nasm -f elf -D__unix__ $< -o $@

としてください.

segment_text
おまじないということで.

proc sum
sumという関数が他からも参照できることを示します. マクロにより外部宣言も同時にされています.

mov eax, [esp+4]
[esp+4]で表されるアドレスに入っている値 (Cで渡したパラメータの一つ目: 今の場合はスタックの項を参照 )をeaxに代入します. 本来はss:[esp+4]という意味なのですがssはデフォルトなので省略可能です.

add eax, [esp+8]
[esp+8]で表されるアドレスに入っている値(Cで渡したパラメータの2つ目:今の場合はb)をeaxに加算します.

ret
呼び出されたところへ戻ります (return). eaxに入っている値がその関数の返り値となります(これはx86におけるCコンパイラの規約がそうなっているからです).

これをsum.asmという名前をつけてアセンブルしてリンクすると動作します.

MASM形式の場合
    .386p
code    segment
    assume cs:code
    public sum
sum    proc near
    mov    eax, [esp+4]
    add    eax, [esp+8]
    ret
sum    endp
code    ends
    end

.386p
386のプロテクトモードであることを明記します. 一つのファイルに一つだけ書きます(もしかしたら最近のMASMだと.flatと書かないといけないかも:未確認).

code segment
...
code ends

これで囲まれた部分に命令文を書きます. "code"はただのラベルなので別の名前でもいいです.

public sum
sumという関数が他からも参照できることを示します. これがないとC言語でのstatic関数と同様, 他からは参照できません.

sum proc near
...
sum endp

sumという関数 (procedure) の中身をこの間に書きます. コンパイラによっては_sumと書かないといけないのもあります. nearというのはこの関数が存在する場所のセレクタと 呼び出すプログラムが存在する場所のセレクタが同じだということです.
特殊なことをしない限りnearでO.K.です.

assume cs:code
コードセレクタcsがcodeを指すよう指示します.

gas (gcc) の場合
.text
.globl sum
sum:
    movl 4(%esp), %eax
    addl 8(%esp), %eax
    ret

.text
MASM形式の場合のcode segment...に相当します.

.globl sum
MASM形式の場合のpublic sumに相当します.

movl 4(%esp), %eax
addl 8(%esp), %eax

intel系とunix系で一番違うところです. 2つのパラメータの順序が反対になります. それからレジスタには全て最初に%がつきます. 後, movとかの命令の後には次が32bit (=long) であることを示す'l'がつきます. 16bitなら'w', 8bitなら'b', 64bitなら'q'です.

これをsum.sとしてgccでアセンブルしてリンクして実行します.

gccのインラインアセンブラを使う

gccの中でちょっとだけasmを使うやり方です. どんな風にアセンブルされるのか分かりにくいので私は使ったことがありませんが, 多分気の持ちようでしょう. 慣れると非常に便利だろうと思います. ただgccのバージョンによっても文法が異なり汎用性は低いです(ここに書いたのはgcc 2.7系の頃).

int add(int a, int b)
{
    int ret;
    asm("movl %0, %%eax"::"g"(a));
    asm("movl %0, %%ecx"::"g"(b));
    asm("addl %ecx, %eax");
    asm("movl %%eax, %0":"=g"(ret));
    return ret;
}

asm("命令":出力パラメータ:入力パラメータ);
この様に命令を記述します. この例のように1行ずつではなく一気にまとめて書くことも出来ます.

"movl %0, %%eax"::"g"(a)
出力パラメータはありません. 入力パラメータ a が%0に入り, それをeaxに代入します. g(...) とあるのはそんなもんなんだと思って下さい. 参照するものがある時は%が2個必要となります.

"addl %ecx, %eax"
入力, 出力パラメータがない時は普通のgasと同じ記述でO.K.です.

"movl %%eax, %0":"=g"(ret)
入力パラメータはありません. 出力パラメータ ret にeaxに代入します.

Visual C++ の場合
int sum(int a, int b)
{
    __asm{
        mov eax, a
        add eax, b
    };
}

__asm{...}
この中にニーモニックを書きます.

mov eax, a
自動的にebpを介してaをeaxに入れてくれます. 楽ですね.
返り値がないと文句を言われますけど, 動作には支障ありません.
#えっコメントが少ないぞ? すんません, 私は殆んど使いません(^^;


スタック

何故mov eax, [esp+4]で一つ目のパラメータを得ることが出来たかと言うと, (x86CPU用の) C コンパイラではたいていそういう決まりになっているからです. 具体的には sum(int a, int b);という関数が呼ばれた瞬間, スタック領域は次のようになっています.

[esp+12] □□□□ (何が入っているか分からない)
[esp+ 8] □□□□ (bの値)
[esp+ 4] □□□□ (aの値)
[esp+ 0] □□□□ (関数が呼ばれた場所)
[esp- 4] □□□□ (何が入っているか分からない)

そしてこのような状態にするための命令がpush, popです.

pushの後に数字を指定すると, espの値を4減らしてからespで指されるアドレスに指定された数字を書き込みます. popは逆にespで指されるアドレスから数字を取りだしespの値を4増やします. つまりsum(int a, int b);を呼び出す部分は次のようになっています.

push [bの値が格納されているアドレス]
push [aの値が格納されているアドレス]
call sum
add  esp, 8

call sumは戻り番値をpushした後sumの番値にジャンプします. そしてretを実行すると, 保存しておいた戻り番値が (eipに) popされます( EIP参照 ).

その後add esp, 8としているのは, 始めの2回のpushでespの値が8だけ減っているのでそれを帳消しにするためです. そうしないと関数を呼び出すたびにどんどんespの値が減っていき, ついには0になってエラーになるからです.

変数を右側からpushしているのは呼び出された関数側では [esp + 4*n]がn番目のパラメータを指すことになり分かりやすいからです (浮動小数などをpushするときはこの限りではありません).

少し細かい話をします.

呼び出された関数の最後でretとしているところで実はret 8とすることができて, これは戻ってくると同時にadd esp, 8も実行せよという命令です. こうすると上でcall sumの後のadd esp, 8は書かなくても良くなります. では何故こうしないのでしょう?

別にそうしてもかまいません. たとえばWindowsAPIの呼び出し規約はそうなっています. それにCのマニュアルと良く読むと, そうできるオプションがついていることもあると思います. ただこれの欠点はprintf(); などのパラメータの数が一定していない関数を扱えなくなるということです (retの後の数字をいくつにしたらいいか分からない). という訳で普通Cでは始めに書いた方法を採用しているのです. 詳細はCとの連係 その2を参照.


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