はじめに
これまで、『インラインアセンブラで学ぶアセンブリ言語』の第1回と第2回を通じて、Microsoft Visual C++のインラインアセンブラを用い、アセンブリ言語の基本について説明してきました。
第3回となる本稿では、インラインアセンブラを用いた流れ制御を解説します。アセンブリ言語は高水準言語における文の概念がなく、すべての命令が単純なオペコードと引数(オペランド)だけで構成されています。この純粋な構造のために、流れ制御を行う場合も、if
文やfor
文のように記述することはできません。基本的にプログラムの流れ制御はすべてC言語で言うところのgoto
文だけで記述し、比較命令や条件ジャンプも個別の命令で行います。
本来のマクロ機能を持たない生のアセンブリ言語ならば、ジャンプ先のプログラムの位置すらも、すべてメモリアドレスで指定しなければなりません。実際に、機械語にアセンブルされた後は、ラベルという情報は持たず、すべてのジャンプ処理は、プログラムがロードされているメモリアドレスを計算して指定します。
しかしながら、こうした作業をすべて手計算で行うのは困難です。変数のメモリサイズの計算とは異なり、アセンブリ言語が機械語にアセンブルされた後のオペコードとオペランドのサイズから、目的のコードが配置されているアドレスを計算する作業は煩雑であり、間違いも許されません。こうした作業は人間ではなく、やはりマクロやコンパイラに委ねるべき仕事です。
幸いインラインアセンブラでは、C言語のラベルを用いてgoto
文のように目的のコードに移動することができます。本稿で、機械語に最も近いアセンブリ言語がどのような命令でプログラムの流れを制御するのか、体験することができます。
単純なジャンプ
条件を指定せずに、無条件に指定した場所に移動するにはJMP
命令を使います。
jmp dest
前述したとおり、本来のJMP
命令は移動先のメモリアドレスを指定するものですが、インラインアセンブラではC言語のラベルを指定することができます。JMP
命令のディスティネーション・オペランドには、C言語のラベルを指定してください。インラインアセンブラのブロック内にラベルを指定することもできるので、C言語の任意のコードから、インラインアセンブラのコードに移動することもできます。
機械語の世界では、「IP」という特殊なレジスタに、実行するべきプログラムのアドレスが格納されています。CPUはプログラムを実行するごとにIPレジスタの値を次の命令に移動させます。JMP
命令は、性質的にIPレジスタの値を変更する命令であると解釈することもできます。
#include <stdio.h> int main() { int value = 0; __asm { jmp label ;label ラベルまでジャンプする add value, 100 ;この命令が飛ばされる label: add value, 1000; } printf("value=%d\n", value); return 0; }
value=1000
「sample01」は、値0で初期化した整数型の変数value
を用意しています。__asm
ブロックでvalue
に100を加算するコードと、1000を加算するコードを用意していますが、JMP
命令が実行された時点でlabel
タグに無条件にジャンプします。そのためadd value, 100
が実行されず、プログラムの結果は1000となります。JMP
命令をコメント化して実行しないようにすれば、直後のADD
命令が実行されて1100という実行結果になることが分かります。
条件分岐
特定の値を調べ、その結果によってプログラムの流れを分岐させるには、最初にCMP
命令を実行します。CMP
命令はオペランドを比較してその結果をレジスタに保存します。
cmp src1 , src2
この命令は、2つのソース・オペランドを持ちます。src1
に第1ソース・オペランドを、src2
に第2ソース・オペランドを指定します。それぞれのソース・オペランドには、レジスタ、メモリアドレス、または即値のいずれかを指定します。
CMP
命令は、指定したソース・オペランドを比較し、EFLAGS
と呼ばれるレジスタに結果を保存します。EFLAGS
は、命令を実行した結果の副作用的な情報をフラグとして保存するレジスタです。このレジスタの値は、直接読み書きをして利用するものではなく、命令の結果として設定されたり、命令の条件として利用されます。
CMP
命令が実行されるとEFLAGS
レジスタのZF
と呼ばれるフラグが設定されます。ZF
とは、ゼロフラグのことで、演算の結果が0であればセットされ、そうでなければ解除されるという性質を持っています。CMP
命令は、一種の減算処理を内部で行いますが、その結果は破棄されます。条件制御は、この演算によって得られた結果を使ってジャンプをすることで実現できます。CMP
命令は一種の減算なので、双方に指定したオペランドの値が一致していれば、結果が0となりZF
フラグがセットされるという仕組みになります。
ZF
フラグの状態によってジャンプするかどうかを決定する命令はJZ
命令と、JNZ
命令があります。
jz dest
jnz dest
JZ
命令は、ZF
フラグがセットされている場合に、ディスティネーション・オペランドに指定された場所にジャンプします。逆にJNZ
命令は、ZF
フラグがセットされていない場合に、ディスティネーション・オペランドに指定された場所にジャンプします。
単純に説明するならば、ある変数AとBが等しいかどうかを調べて、その結果に応じて実行するコードを変更したい場合、CMP
命令でAとBを比較し、その後にJZ
命令を用いる形になるでしょう。
#include <stdio.h> int main() { int value; char * tc = "value は 100 でした\n"; char * fc = "value は 100 ではありませんでした\n"; char * result; printf("value 変数に代入する任意の値を入力してください>"); scanf("%d", &value); __asm { cmp value, 100 ;value が 100 と等しいかどうかを調べる jz cmp_true ;value が 100 ならジャンプする mov ebx, fc; mov result, ebx; jmp end ;end にジャンプする cmp_true: mov ebx, tc; mov result, ebx; end: } printf(result); return 0; }
C:\...>sample02 value 変数に代入する任意の値を入力してください>10 value は 100 ではありませんでした C:\...>sample02 value 変数に代入する任意の値を入力してください>100 value は 100 でした
「sample02」は、最初にscanf()
関数を用いてユーザーにvalue
変数に代入する値を入力してもらいます。その後__asm
ブロック内でvalue
変数の値を調べ、入力された値が100と等しいかどうかで、結果として出力する変数を設定します。value
変数に入力された値が100と等しければJZ
命令によってcmp_true
ラベルまで移動し、tc
変数がresult
変数に設定されます。そうでなければ、fc
変数がresult
変数に設定され、下部のtc
をresult
に設定するプログラムがそのまま実行されないようにするためにJMP
命令でend
ラベルまで移動しています。これらの作業は、C言語のif-else
文と同じです。
JZ
命令やJNZ
命令では、値が等しいかどうかを調べてジャンプしていましたが、当然、値がより小さいか、またはより大きいかを調べることも可能です。こうした判定処理もJZ
命令と同様にCMP
命令による結果がEFLAGS
レジスタに保存され、EFLAGS
レジスタ内の各種フラグの値を調べて実行されます。ジャンプ命令は非常に数が多いので詳細は割愛しますが、使い方はJZ
命令と同じです。
命令 | ジャンプ条件 |
JA | より上(CF = 0 & ZF = 0) |
JAE | より上か等しい(CF = 0) |
JB | より下(CF = 1) |
JBE | より下か等しい(CF = 1 | ZF = 1) |
JC | キャリーがある(CF = 1) |
JCXZ | CXレジスタが0 |
JE | 等しい(ZF = 1) |
JG | より大きい(ZF = 0 & SF = OF) |
JGE | より大きいか等しい(SD = OF) |
JL | より小さい(SF ! OF) |
JLE | より小さいか等しい(ZF = 1 | SF ! OF) |
JNA | より上でない(CF = 1 | ZF = 1) |
JNAE | より上でなく等しい(CF = 1) |
JNB | より下でない(CF = 0) |
JNBE | より下でなく等しい(CF = 0 & ZF = 0) |
JNC | キャリーがない(CF = 0) |
JNE | 等しくない(ZF = 0) |
JNG | より大きくない(ZF = 1 | SF ! OF) |
JNGE | より大きくなく等しくない(SF ! OF) |
JNL | より小さくない(SF = OF) |
JNLE | より小さくなく等しくない(ZF = 0 & SF = OF) |
JNO | オーバーフローがない(OF = 0) |
JNP | パリティがない(PF = 0) |
JNS | 符号がない(SF = 0) |
JNZ | ゼロではない(ZF = 0) |
JO | オーバーフローがある(PF = 1) |
JP | パリティがある(PF = 1) |
JPE | パリティが偶数(PF = 1) |
JPO | パリティが基数(PF = 0) |
JS | 符合がある(SF = 1) |
JZ | ゼロである(ZF = 1) |
上の表に示した各種ジャンプ命令は、演算の結果によって設定されるEFLAGS
レジスタのフラグを調べて、その結果が真であればジャンプするという性質を持ちます。通常は、CMP
命令で値を比較した後、ジャンプ命令で条件に従ってジャンプするかしないかを決定するという使い方になります。
#include <stdio.h> int main() { int a, b; char * r1 = "a は b より大きい\n"; char * r2 = "a は b より小さい\n"; char * r3 = "a は b と等しい\n"; char * result; printf("a の値を入力してください>"); scanf("%d", &a); printf("b の値を入力してください>"); scanf("%d", &b); __asm { mov eax, a; cmp eax, b; ja label1; jb label2; mov ebx, r3; mov result, ebx; jmp end; label1: mov ebx, r1; mov result, ebx; jmp end; label2: mov ebx, r2; mov result, ebx; end: } printf(result); return 0; }
C:\...>sample03 a の値を入力してください>10 b の値を入力してください>100 a は b より小さい a の値を入力してください>100 b の値を入力してください>10 a は b より大きい a の値を入力してください>100 b の値を入力してください>100 a は b と等しい
「sample03」は、scanf()
関数からユーザーに変数a
とb
の値を入力してもらい、a
とb
の値を比較して、その結果に応じた文字列を出力するというプログラムです。CMP
命令でa
とb
を比較し、その後JA
命令、またはJB
命令で、目的のコードにジャンプします。