2.1 はじめに

この章では、アセンブリ言語の基礎を解説していきます。 アセンブリ言語を全く知らない方はもちろん、Windows用のプログラムは組めないがMS-DOS用のプログラムなら組めるという方にも役に立つと思います。 Windows用のプログラムをアセンブリ言語で組める方は読み飛ばして頂いて構いませんが、そうでない方は一度目を通して下さい。

2.2 レジスタ

仮に、以下のようなHSPのプログラムがあったとします。

a=123
b=456
c=789
d=a*b+c
print d
stop

このプログラムの4行目に注目して下さい。 a*b+cを計算してdに代入しています。 こう書くといかにもa*b+cを一度に計算しているように見えますが、人間が同じ計算をしたらどうでしょうか? まずa*bを計算して、その結果にcを足しますよね? 実は人間と同じように、コンピュータの内部でもまずa*bを計算し、その結果にcを足しているのです。 ということは、計算の途中の値をメモしておくための計算用紙にあたるものが必要になりますよね? それには、CPUの内部にあるレジスタと呼ばれるものを使います。 レジスタには、計算の途中の値を覚えておく為に使われるレジスタのほかにも、様々な用途を持ったレジスタがあります。 ここでは全てのレジスタについては解説せず、HSPのプラグインを組む為に覚えておかなければならないレジスタについてのみ解説します。 HSPのプラグインを組む為に覚えておかなければならないレジスタには以下のレジスタがあります。

レジスタの名前 主な用途
EAX 計算の途中の値を覚えておくのに使います。
HSPのシステム変数STATに値を返したりするのにも使います。
EBX 計算の途中の値を覚えておく場所がEAXの他にも必要なときに使います。
ECX ある処理を繰り返したい時、回数をカウントするのに使います。
EDX 計算の途中の値を覚えておく場所がEAXの他にも必要なときに使います。
ESI メモリのアドレスを覚えておくのに使います。
EDI メモリのアドレスを覚えておくのに使います。
ESP スタックポインタのアドレスを覚えておくのに使います。
スタックについては後ほど説明します。
EBP スタックフレームのアドレスを覚えておくのに使います。
スタックフレームについては第4章で説明します。
EFLAGS CPUの状態を表すフラグが入っています。
計算用には使用できません。

この表に書かれているレジスタは全て32ビットのレジスタですが、下位16ビットをそれぞれAX,BX,CX,DX,SI,DI,SP,BP,FLAGSとして扱う事もできます。 また、AX,BX,CX,DXの上位8ビットをそれぞれAH,BH,CH,DH、下位8ビットをそれぞれAL,BL,CL,DLとして扱う事もできます。

2.3 オペランド

HSPでは命令の名前の後ろに書く変数名や数値などの事を「パラメータ」と呼びますが、アセンブリ言語では「オペランド」と呼びます。 movやaddなどのように2つのオペランドを持つ命令について、1つ目のオペランドの事を第1オペランド、2つ目のオペランドのことを第2オペランドと呼びます。 オペランドには大きく分けるとレジスタ、メモリ、即値の3種類があります。 即値とは、いわゆる定数のことです。 メモリの番地を指定するには以下の方法があります。

アドレスの指定に使えるレジスタは、eax,ebx,ecx,edx,esi,edi,esp,ebpの8つです。 また、即値の代わりにラベル名を使う事もできます。

2.4 即値の表し方

16進数を表したい時、HSPでは数値の前に$または0xを付けますが、アセンブリ言語では数値の後ろhまたはHを付けます。 例えば、16進数の100(10進数の256)を表すには100hと書きます。 但し、先頭の文字がアルファベットだと変数名とみなされてしまうので注意して下さい。 例えば16進数のf0(10進数の240)を表すのにf0hと書いてしまうと、f0hという名前の変数名であるとみなされてしまいます。 これを回避するには、頭に0を付けて0f0hと書きます。

2進数を表したい時、HSPでは数値の前に%または0bを付けますが、アセンブリ言語では数値の後ろbまたはBを付けます。 例えば、2進数の1111(10進数の15)を表すには1111bと書きます。

8進数を表したい時、アセンブリ言語では数値の後ろにoまたはOを付けます。 例えば、8進数の10(10進数の8)を表すには10oと書きます。

10進数を表したい時は、HSPやC言語の場合と同様に数値をそのまま書きます。 例えば、10進数の100を表すには100と書きます。

文字コードを表したい時は、HSPやC言語の場合と同様に'で文字を囲みます。 例えば、aの文字コードを表すには'a'と書きます。

2.5 ラベルの書き方

特定の場所に名前を付ける時、HSPではラベル名の頭に*(アスタリスク)を付けますが、アセンブリ言語ではラベル名の末尾に:(コロン)を付けます。 逆に特定の場所にジャンプさせたい場合、HSPではラベル名の頭に*(アスタリスク)を付けますが、アセンブリ言語では名前の頭に*(アスタリスク)を付けたり、名前の末尾に:(コロン)を付けたりはしません。

また、HSPでは*@でローカルラベルを定義しますが、MASM32では@@:でローカルラベルを定義します。 逆にローカルラベルを定義した場所へジャンプさせたい場合、HSPでは*bや*fと書きます(*back、*forwardとも書きます)が、MASM32では@bや@fと書きます。

2.6 第1章のサンプル

第1章では、具体的な処理の中身については解説しませんでした。 第1章の最後に「具体的な処理の部分については、次の章から解説していきます。」と書いたので、第1章のサンプルについて解説します。

ASMHSP.ASM
.486
.model		flat,stdcall
.code

DLLMain		proc	p1,p2,p3
		mov	eax,1
		ret
DLLMain		endp

asmhsp		proc	export p1:ptr,p2,p3,p4
		push	esi
		mov	esi,p1
		mov	eax,p2
		add	eax,p3
		add	eax,p4
		mov	[esi],eax
		mov	eax,0
		pop	esi
		ret
asmhsp		endp

end		DLLMain

さて、このプログラムは何をするプログラムだったかというと、p2+p3+p4を計算してp1に代入するプログラムでしたね。 では、具体的にどのような処理をしているのか見ていきましょう。

まず、push命令ですが、これについては後ほど説明します。
次のmov esi,p1というのは、p1の値をesiに代入しなさい、という命令です。
その次のmov eax,p2も同様に、eaxにp2の値を代入しなさい、という命令です。
そしてその次のadd,eax,p3は、eaxにp3の値を加えなさい、という命令です。
その次のadd eax,p4も同様に、eaxにp4の値を加えなさい、という意味です。

では、その次のmov [esi],eaxはどうでしょうか? またまたmov命令ですが、今度はesiに括弧が付いていますね。 これは一体どういう意味でしょう? これは、メモリの中のesiが指し示しているアドレスに、eaxの値を代入しなさい、という意味です。 例えば、この時のesiの値が100だったとすると、メモリの中の100番地にeaxの値が代入されます。 実際にはp1の値がesiに入っているわけですから、このプラグインを呼び出す側でp1に指定された変数のアドレスがesiに入っています。 そこへeaxの値を代入するわけですから、これでめでたくp2+p3+p4の値をp1へ代入する事ができましたね。

さて、次のmov eax,0はもうお分かりですね。eaxに0を代入しなさい、という意味です。
次のpop命令については後ほど説明します。
その次の命令で、プラグイン側の処理を終了しHSP側に制御を戻します。
この時eaxには0が入っているので、HSPのシステム変数STATの値も0になります。

2.7 スタック

第一章のサンプルでは、esiレジスタとeaxレジスタを使いました。 eaxレジスタは結果を返すのに使っているからいいとして、もしもHSP側が何らかの処理にesiレジスタを使っていたらどうでしょうか? プラグイン側でesiの値を書き換えてしまっては困るかもしれません。 そこで、esiの値を退避しておき、後で元の値に戻しておくと安全です。 その為にはスタックを使います。

上のサンプルを見て下さい。 先程説明を後回しにしたpush esiというのが、esiの値をスタックへ退避しなさい、という命令です。 そして、pop esiというのが、スタックへ退避されている値をesiへ戻しなさい、という命令です。 スタック領域はメモリの一部が自動的に割り当てられている為、自分でスタック領域を確保する必要はありません。

では、push命令やpop命令を実行するとスタックの中身はどうなるのでしょうか? push esiが実行されると、espの値が4減らされ、espが示すアドレスにesiの値が退避されます。 例えばespの値が100だったとすると、espの値は96になり、96番地にesiの値が退避されます。 逆に、pop esiが実行されると、espが示すアドレスにあるデータがesiに代入され、espの値が4増やされます。 例えばespの値が96だったとすると、96番地にあるデータがesiに代入され、espの値は100になります。

スタックを使う上で注意しなければならない事が2つあります。 1つ目は、複数のレジスタを退避したとき、pushとは逆の順序でpopしなければならないことです。 例えば、eax,ebx,ecx,edxの順にpushした時は、edx,ecx,ebx,eaxの順にpopしないと元通りにはなりません。 2つ目は、スタック領域の大きさには限りがある事です。 再帰を使わない場合は心配する必要はないかと思いますが、再帰の回数が多くなるとスタック領域が足らなくなる場合があります。 (再帰については、C言語の入門書やアルゴリズムの本などで詳しく解説されているかと思いますが、ここでは説明しません。)

2.8 おわりに

第2章には「アセンブリ言語の基礎」というタイトルをつけましたが、基本的な命令についてはごく一部しか解説していません。 これだけでは、第1章のサンプル程度のものしか作れないのは言うまでもありません。 しかし、第2章で解説した事は非常に重要な事ばかりなので、しっかり理解しておいて下さい。