繰り返し制御
繰り返し制御も、基本的にはジャンプ命令を使って実現することができます。C言語でfor文を使うときと同様に、アセンブリ言語の場合でも、繰り返しの条件とするためのカウント変数を用意する必要があります。通常は、ECXレジスタを使って、繰り返しを制御するカウンタ処理を行います。例えば、ECXレジスタに任意の値を設定し、繰り返しを行うたびにデクリメントするように仕掛ければ、ループカウンタとして利用することができます。一定の命令群を対象に繰り返しを行う場合、命令を実行した後にCMP命令でECXと0を比較し、JAやJNZ命令を使って命令の開始地点にジャンプさせます。
#include <stdio.h> int main() { char result[32]; char * p = result; __asm { mov al, 'A'; mov ecx, 26; label: mov ebx, p; mov [ebx], al; inc p; inc al; cmp ecx, 0 ;カウンタが 0 かどうか dec ecx ;ECX レジスタをデクリメント ja label ;カウンタが 0 以上なら label に戻る mov ebx, p; mov [ebx], 0; } printf("%s\n", result); return 0; }
ABCDEFGHIJKLMNOPQRSTUVWXYZ
「sample04」は、32バイトのchar型の配列resultに対して、先頭からA~Zのアルファベットをストアするプログラムです。__asmブロック内ではresult配列の先頭へのポインタpを利用し、pから間接的にアルファベットをストアしています。繰り返し処理を行うたびにpポインタをインクリメントすることで、ストア先の配列インデックスを計算させています。
繰り返しの条件にはECXレジスタを利用しています。最初にECXレジスタに26を設定し、繰り返しを行うたびにECXをデクリメントしています。これをECXが0になるまで繰り返すことで、result配列変数内にA~Zまでの値をストアしています。ECXの初期値26を変更すれば、resultに保存されるアルファベットの数を変更することができます。
「sample04」では、カウンタのデクリメント処理や条件判定、ジャンプ処理をすべてコードで管理していましたが、LOOP命令を使えばこれらの作業を自動的に行ってくれます。
loop dest
この命令は、ECXレジスタをデクリメントし、ECXレジスタの値が0でなければディスティネーション・オペランドに指定されているアドレスにジャンプするというものです。カウント処理と比較処理を省略することができるため、通常は「sample04」のような処理を行う場合はLOOP命令を使います。しかし、C言語のように繰り返し処理を行う一連の命令群をブロック化できるような機能はなく、ジャンプ先は任意のラベル(実質的にはアドレス)を指定することになります。
#include <stdio.h> int main() { char result[32]; char * p = result; __asm { mov al, 'a'; mov ecx, 26; label: mov ebx, p; mov [ebx], al; inc p; inc al; loop label; mov ebx, p; mov [ebx], 0; } printf("%s\n", result); return 0; }
abcdefghijklmnopqrstuvwxyz
「sample05」は、「sample04」を書き換えてLOOP命令を使ったプログラムです。基本的な動作は変わりませんが、ECXレジスタのデクリメント処理と、CMP命令による比較処理が省略されていることが分かります。
サブルーチン
アセンブリ言語の世界にも、C言語の関数のような概念が存在します。必要なときに別の場所から呼び出すことができる一連の命令群のことを「サブルーチン」と呼びます。サブルーチンは必要なときにサブルーチンの外から呼び出すことができ、処理が終了した時点でサブルーチンを呼び出したコードに制御を復帰させることができます。機能的には関数と同じようなものだと考えてかまいません。
インラインアセンブラではサブルーチンを使うというシナリオは存在せず、基本的にはC言語の関数を使うべきですが、本稿ではアセンブリ言語の学習のために、インラインアセンブラ内でサブルーチンを作成してみましょう。
アセンブリ言語の世界でのサブルーチンの呼び出しとは、サブルーチンの先頭のコードにジャンプするということを表します。IPレジスタの値をサブルーチンの先頭アドレスに設定することで、サブルーチンに制御を移すことができます。しかし、JMP命令でジャンプするだけでは呼び出したコードに復帰することができません。そこで、一連のルーチンへのジャンプにはJMP命令ではなくCALL命令を使います。
call dest
JMP命令と同様に、ディスティネーション・オペランドにはサブルーチンのアドレスを指定します。インラインアセンブラでは、アドレスを直接指定することはないので、CALL命令のオペランドにはラベルを指定します。サブルーチンのコードにジャンプするところまではJMP命令と同じですが、CALL命令はJMP命令とは違い、CALL命令を実行したアドレスを保存します。サブルーチンからはRET命令を使うことによって、現在のコードをCALLしたアドレスに復帰することができます。
ret
今回利用するRET命令には、オペランドは必要ありません。RET命令を実行すると、現在のコードを呼び出したCALL命令のアドレスまで戻ります。CALL命令とRET命令は常に組み合わせて使う命令です。これらの命令を利用することで、C言語における関数のように、一定の処理をひとつにまとめ、再利用することができるようになります。
#include <stdio.h> int main() { int r1, r2, r3, r4; __asm { jmp end; setZero: mov eax, 0; mov ebx, 0; mov ecx, 0; mov edx, 0; ret; setRegister: mov r1, eax; mov r2, ebx; mov r3, ecx; mov r4, edx; ret; end: } __asm call setRegister printf("EAX=%d, EBX=%d, ECX=%d, EDX=%d\n", r1, r2, r3, r4); __asm { call setZero call setRegister } printf("EAX=%d, EBX=%d, ECX=%d, EDX=%d\n", r1, r2, r3, r4); return 0; }
EAX=210704, EBX=2088806985, ECX=1, EDX=4248728 EAX=0, EBX=0, ECX=0, EDX=0
「sample06」は、最初の__asmブロック内にあるsetZeroラベルとsetRegisterラベル以降のコードを、一種のサブルーチンとみなしています。setZero以降は、すべての汎用レジスタを0に設定するというものであり、setRegister以降はr1~r4までの変数にレジスタの内容をコピーするというものです。これらのサブルーチンはRET命令で終了しているため、CALL命令を使って呼び出すことができます。
CALL命令とRET命令の組み合わせは入れ子にすることができます。つまり、CALL命令に呼び出されたサブルーチンの中で、さらに別のサブルーチンをCALLすることができます。このような場合でも、RET命令は、最後に現在実行中のコードをCALLした呼び出し元に復帰することができます。
#include <stdio.h> int main() { int a, b, c; __asm call routine1; printf("A=%d, B=%d, C=%d\n", a, b, c); return 0; __asm { routine1: mov a, 10; call routine2; ret; routine2: mov b, 20; call routine3; ret; routine3: mov c, 30; ret } }
A=10, B=20, C=30
「sample07」は、main()関数内の最初の__asmブロックから、return文より後のroutine1を呼び出しています。routine1では、変数aに値10をストアした後にroutine2を呼び出しています。routine2は、変数bに値20をストアしてからroutine3を呼び出しています。そして、routine3で、変数bに値30をストアしてから、ようやくRET命令で呼び出し元に復帰していきます。routine3のRET命令が実行されると、routine3を呼び出したroutine2のCALL命令の直後に戻ります。routine2のCALL命令の直後の命令はRET命令なので、さらにroutine1に復帰し、routine1のRET命令が実行され、main()関数の最初の__asmブロックまで戻ってくる、という流れになります。
