SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

インラインアセンブラで学ぶアセンブリ言語

インラインアセンブラで学ぶアセンブリ言語 第3回

アセンブリ言語による分岐や繰り返し

スタック

 CALL命令とRET命令が対になって動作しています。RET命令が、正しくCALL命令を実行したアドレスにIPレジスタの値を戻せるのは、スタックを使って、最後にCALL命令を実行したアドレスを保存しているためです。スタックのアルゴリズムは、先入れ後出しです。つまり、最後にCALL命令でプッシュされたアドレスが、RET命令によってポップされるのです。

 CPUは、アドレスの計算を必要とせずに値をスタックにプッシュしたり、ポップする機能を提供しています。スタックに値をプッシュするにはPUSH命令を、スタックから値をポップするにはPOP命令を使います。

push src
pop dest

 PUSH命令は、ソース・オペランドにプッシュする任意のレジスタ、メモリアドレス、または即値を指定します。POP命令は、ポップした値を格納するディスティネーション・オペランドを指定します。最後にプッシュされた値が、POP命令に指定したディスティネーションに保存されます。

 CPUは、スタックを管理するためのESPレジスタを提供しています。ESPレジスタは、現在のスタックのアドレスを保存し、PUSH命令やPOP命令のたびに32ビット単位でインクリメント、またはデクリメントされます。

 CALL命令とRET命令のいずれも、スタックを利用してサブルーチンの呼び出した復帰を行っているので、スタックは無秩序に利用できる空間ではありません。

sample08
#include <stdio.h>

int main() {
    int r, v;

    __asm mov r, esp;
    printf("ESP=%p\n", r);

    __asm {
        push 10;
        mov r, esp;
    }
    printf("Push ESP=%p\n", r);

    __asm {
        push 20;
        mov r, esp;
    }
    printf("Push ESP=%p\n", r);

    __asm {
        push 30;
        mov r, esp;
    }
    printf("Push ESP=%p\n", r);

    __asm {
        pop v;
        mov r, esp;
    }
    printf("Pop V=%d, ESP=%p\n", v, r);

    __asm {
        pop v;
        mov r, esp;
    }
    printf("Pop V=%d, ESP=%p\n", v, r);

    __asm {
        pop v;
        mov r, esp;
    }
    printf("Pop V=%d, ESP=%p\n", v, r);

    return 0;
}
実行結果
ESP=0013FF68
Push ESP=0013FF64
Push ESP=0013FF60
Push ESP=0013FF5C
Pop V=30, ESP=0013FF60
Pop V=20, ESP=0013FF64
Pop V=10, ESP=0013FF68

 「sample08」は、PUSH命令とESPレジスタの関係を確認するためのサンプルプログラムです。PUSH命令を使ってスタックに値をプッシュすると、ESPレジスタのアドレスが32ビット単位、つまり4バイト単位でデクリメントされていることが確認できます。スタックにプッシュすると、自動的にESPレジスタのアドレスがオペランドのサイズに従ってデクリメントされています。これに対して、スタックから値をポップすると4バイト単位でESPレジスタがインクリメントされています。

 このように、PUSH命令と POP命令は、ESPレジスタを操作しながら、値をスタックに積んだり、スタックから値を取り出したりしています。CALL命令と RET命令も、PUSHPOP同様に、スタックに呼び出し元のアドレスをプッシュし、復帰するときにポップしているのです。

 こうした内情から、サブルーチンを呼び出しているコードの中で、無秩序にスタックを操作してはいけません。RETが行われる前にアドレスがポップされてしまったり、復帰するためのアドレス以外がスタックに積まれている状態でRET命令を実行するなどした場合、適切なアドレスに復帰することができず強制終了することになるでしょう。

関数の呼び出し

 CALL命令のオペランドに渡す値は、本来ならばサブルーチンが配置されているメモリ上のアドレスです。CALL命令のオペランドにはC言語のラベルを指定することができましたが、そもそもC言語には関数へのポインタというものが存在します。関数へのポインタは、その関数のメモリアドレスを表します。よって、関数へのポインタをCALL命令のオペランドに指定することで、C言語の関数を呼び出すことができます。

sample09
#include <stdio.h>

void func() {
    printf("I have no interest in ordinary humans.\n");
}

int main() {
    __asm call func;
    return 0;
}
実行結果
I have no interest in ordinary humans.

 「sample09」は、引数を受け取らず、戻り値を返さないfunc()関数をCALL命令で呼び出しています。関数名だけを指定した場合、その関数へのポインタを表します。通常、C言語のインラインアセンブラでは、別の関数をアセンブリ言語から呼び出す目的でCALL命令を使います。

 関数に引数を渡す場合、スタックを利用します。引数を取得する関数は、スタックから引数を受け取ります。関数は、仮引数リストの逆から順番に、スタックから引数を取り出します。よって、引数を持つ関数を呼び出す場合は、関数の仮引数リストの宣言とは逆の順番で、スタックに引数をプッシュします。

 関数から制御が戻ってきた後、関数を呼び出したコードは適切にスタックを戻さなければなりません。前述したように、スタックはRET命令で利用されているため、関数に渡した引数がスタックにそのまま残っている場合は、適切に現在の関数からRET命令で復帰することができなくなってしまいます。関数を呼び出すためにスタックにプッシュした引数の数だけ、使っていない適当なレジスタにポップしてください。

 関数が戻り値を返す場合、EAXレジスタに戻り値の値が格納されています。

sample10
#include <stdio.h>

int func(int arg1, int arg2, int arg3) {
    printf("arg1=%d, arg2=%d, arg3=%d\n", arg1, arg2, arg3);
    return arg1 + arg2 + arg3;
}

int main() {
    int result;

    __asm {
        push 1000;
        push 100;
        push 10;
        call func;

        pop ebx;
        pop ebx;
        pop ebx;

        mov result, eax;
    }

    printf("result=%d\n", result);
    return 0;
}
実行結果
arg1=10, arg2=100, arg3=1000
result=1110

 「sample10」は、main()関数内にあるインラインアセンブラのコードからfunc()関数を呼び出すプログラムです。__asmブロック内では、func()関数に渡すための引数をプッシュし、その後CALL命令を使ってfunc()関数を呼び出しています。

 func()関数は、渡された3つの引数を出力し、引数をすべて加算した結果を戻り値として返します。CALL命令から制御が戻され__asmブロックに戻ると、func()関数に渡した引数をスタックからポップし、最後にfunc()関数の戻り値が格納されているEAXレジスタの値をresult変数にストアしています。実行結果を見れば、func()関数に適切に値が渡され、戻り値を受け取っていることが確認できます。

 上記のようにC言語の関数を呼び出せば、アセンブリ言語を使ってC言語の標準関数を呼び出すこともできます。C言語の関数が、機械語の世界でどのように動作しているのか、上記のサンプルから感じ取ることができると思います。

最後に

 レジスタを直接操作するという作業を除いて、本稿で解説してきたインラインアセンブラの機能はすべてC言語でも行うことができます。むしろ、C言語で記述した方がより安定していて、汎用性も高く、保守も容易です。特別な事情がない限り、実践の開発で積極的にインラインアセンブラを使う理由というものはないでしょう。

 しかし、例えばIntelのCPUは、マルチメディア処理専用のレジスタや命令を用意しています。こうした局所的な命令を明示的に利用したい場合、C言語ではなくアセンブリ言語を使わなければなりません。高度な処理が要求されるゲームプログラミングなどでは、現在でもアセンブリ言語の活躍の場が存在することでしょう。また、実践の機会がなかったとしても、アセンブリ言語や機械語は、コンピュータの原理に直結しています。アセンブリ言語を学習することで、コンピュータの原理を正しく理解している技術者として、ステップアップすることができます。

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
インラインアセンブラで学ぶアセンブリ言語連載記事一覧

もっと読む

この記事の著者

赤坂 玲音(アカサカ レオン)

平成13年度「全国高校生・専門学校生プログラミングコンテスト 高校生プログラミングの部」にて最優秀賞を受賞。2005 年度~ Microsoft Most Variable Professional Visual Developer - Visual C++。プログラミング入門サイト WisdomSoft の管理人。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/485 2006/08/29 00:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー