お読みになる前に
本稿の内容は、読者の環境にMASM32がインストールされていることを前提にしています。まだインストールしていない場合は、http://www.masm32.com/から入手してください。
はじめに
シリーズ第1回では、Microsoft Assemblerを使用してアセンブラファイルをコンパイルできるようVisual Studioをセットアップする方法を説明しました。第2回では、アセンブラという言語そのものと、アセンブラに含まれているいくつかの命令について解説したいと思います。
変数
アセンブラには変数はありません。少なくとも、C++で使われているような意味での変数はありません。アセンブラでは、レジスタとメモリアドレスを使用します。つまり、アセンブラを書くということは、プロセッサと同じ言語を話すということです。この点はぜひ頭に入れておいてください。
たとえば、プロセッサはnMyInteger
という整数値があることを知りません。CMyClass
というクラスがあることも知りません。プロセッサが認識しているのはレジスタだけであり、プロセッサがは与えられたアドレスのメモリにアクセスします。
では、レジスタとは一体何なのでしょうか。そして、どうやったらメモリにアクセスできるのでしょうか。
レジスタ
レジスタは変数に似ていますが、プロセッサのみが使用するという点が異なります。先ほど「高等言語のような意味での変数は存在しない」と言ったのはそういうわけです。プロセッサチップ上には一定数のレジスタのみが存在します。レジスタは「プロセッサチップ上に物理的に存在し、プロセッサ用にハードコーディングされた変数である」と考えるとわかりやすいでしょう。
これらのレジスタは、決まったサイズの数値を表すことができます。このサイズはプロセッサのビット数によって決まります。つまり、32ビットプロセッサの場合は32ビットになります。C++用語では、この一定サイズの数値のことをDWORD
と呼びます。
この数値には符号は付いていません。負数を表さなければならないときは、「0x100000000+負数」で表現します。したがって、-1は0xFFFFFFFF、-2は0xFFFFFFFEで表現されます(以下同様です)。
最新のIntelプロセッサには非常に多くのレジスタがありますが、アプリケーション内で使用するレジスタは次の6つだけです。
eax
- アキュムレータレジスタebx
- ベースレジスタecx
- カウンタレジスタedx
- データレジスタesi
- メモリ操作用のソースレジスタedi
- メモリ操作用のデスティネーションレジスタ
レジスタeax
、ebx
、ecx
、edx
は、参照方法を変更することで、そのレジスタを構成するバイトへと分割することができます。たとえば、アキュムレータレジスタ(Aレジスタとも呼ばれます)の場合は次のようになります。
al
:eax
レジスタ内の下位ワードの下位(lower
)バイトah
:eax
レジスタ内の下位ワードの上位(higher
)バイトax
:eax
レジスタの下位ワード(つまり2バイト)eax
: レジスタ全体 (4バイト)
(ah << 8) + al
ebx
、ecx
、edx
に対しても、次の図のように同様の命名規則が適用されます(esi
およびedi
には当てはまりません)。
これらの名前は、プロセッサの発展経緯に拠ります。レジスタ名の先頭の「e」は「拡張(extended)レジスタ」を表しています。つまり、16ビットプロセッサから32ビットプロセッサに移行したときの、それぞれのレジスタの32ビット版という意味です。16ビットプロセッサで使用できるレジスタはax
、bx
、cx
、dx
などだけでした。32ビットプロセッサが登場したときに、新たに使用可能になった16ビット分を「e」という接頭辞で表すようになりました。ただし、レジスタの上位16ビットに直接アクセスする方法はありません。
mov(移動)命令
まず、最も単純なmov
(移動)命令から見ていくことにしましょう。mov
命令は、プロセッサ内部で値を移動する方法を表します。次に例を示します。
mov eax, 100
この命令では、100という値をeax
レジスタに「移動」しています。これはeax=100
と同じことです。mov
命令の構文は次のようになります。
mov (destination), (source)
source
とdestination
は同じサイズ(ビット数)でなければなりません。次にmov
命令の例をいくつか示します。
mov al, bl ; move the lower byte of ebx into the lower byte ; of eax mov al, 0ffh ; move 0xFF into the lower byte of eax mov ah, 0ffh ; move 0xFF into the high byte of the low word ; (2-bytes) of eax mov ax, 0ffffh ; move 0xFFFF into the low word of eax mov eax, 0ffffh ; move 0xFFFF into eax
メモリの内容をレジスタに移動したり、その逆を行ったりすることもできます。その場合は、「メモリの内容」を表すために大カッコを使用します。移動されるバイト数はレジスタ名によって決まります。
mov al, [esi] ; move the byte contained in the memory address ; in register esi into the lower byte of eax mov [edi], bl ; move the byte value in the lowest byte of ebx ; into the memory address in register edi mov cx, [esi] ; move the word (2-byte) value contained in the ; memory address of register esi into the lower ; word of ecx mov [edi], edx ; move the dword (4-byte) value contained in edx ; into the memory address contained in register edi
また、大カッコの演算子を使用するときにオフセットを含めることもできます。
mov al, [esi + 3] ; move the byte contained in the memory address ; in register esi + 3 into the lower byte of eax mov [edi + 2], dx ; move the lower word (2-bytes) contained in ; edx into the memory address contained in the ; register edi + 2
関数
関数は次の形式で宣言します。
TestProc proc dwValue1:DWORD, wValue2:WORD, bValue3:BYTE ret TestProc endp
このコード例は空の関数ですが、基本的な形式はどの関数でも同じです。まず関数名を指定し、その後にproc
と指定します。関数のパラメータは、proc
の後に<name>:<type>
という形式で必要な数だけ指定します。パラメータの型として使用できるのはDWORD
、WORD
、BYTE
です。
関数の終わりには、関数名とendp
を含む行を記述します。
ret
文は戻り値を返す文です。言い換えれば、この文のところで関数が終了します。ret
文は関数の最後に記述しなければなりません。
この関数をC++から呼び出す場合は、関数から戻る前に、レジスタebx
、esi
、edi
の元の値を復元する必要があります。これを行うには、通常はpush
とpop
を使用します(詳しくは後述)。
関数の戻り値はeax
レジスタに格納されます。ほとんどの命令では、関数のパラメータに名前でアクセスすることができます。次に例を示します。
TestProc proc dwValue1:DWORD, dwValue2:DWORD mov eax, dwValue1 add eax, dwValue2 ret TestProc endp
この関数では、dwValue1
とdwValue2
を加算して、結果を返します。
C++からアセンブラ関数にアクセスするには、同じ名前とパラメータを持つ関数を宣言する必要があります。C++でのパラメータのサイズは、アセンブラコード内で定義したパラメータのサイズに等しくなければなりません。さらに、extern "C"
として宣言し、stdcall
呼び出し規則を使用する必要があります。たとえば上記のアセンブラ関数に対するC++宣言は次のようになります。
extern "C" unsigned int __stdcall TestProc(unsigned int dwValue1, unsigned int dwValue2);
ポインタを渡す場合は、そのポインタをアセンブラ関数のDWORD
パラメータとして宣言します(32ビットオペレーティングシステムではポインタのサイズが32ビットなので)。同様に、char
はBYTE
として渡し、WCHAR
はWORD
として渡すようにします。
アセンブラ関数を静的DLLからエクスポートする設計にした場合は、C++内で関数を宣言する必要はありません。DLLの.defファイルに関数名を含めるだけで、この方法で宣言した他のC++関数と同じように使用することができます。
スタックとpush命令、pop命令
プロセッサには「スタック」と呼ばれる機構があります。push
命令とpop
命令を使用すると、このスタックに対してレジスタ、定数、メモリの内容をプッシュ/ポップすることができます。
スタックは、使用可能なレジスタの数の少なさを補うために用意された機構です。スタックを使用すると、レジスタの内容を効率的かつ迅速な方法で保存/取得することができます。
簡単に言えば、スタックはファーストイン-ラストアウト方式の値キューです。push
命令ではキューの一番上に値を追加し、pop
命令ではキューの一番上にある値を削除して、レジスタまたはメモリアドレスに格納します。次に例を示します。
TestFunction proc mov eax, 100 push eax ; Stack now contains { 100 } mov eax, 200 push eax ; Stack now contains { 200, 100 } mov eax, 300 push eax ; Stack now contains ( 300, 200, 100 } pop eax ; eax = 300, stack = { 200, 100 } pop eax ; eax = 200, stack = { 100 } pop eax ; eax = 100, stack = { } ret TestFunction endp
スタックの一般的な用途は、関数を終了する前にレジスタebx、esi、ediの値を復元することです。次に例を示します。
TestFunction proc push ebx push esi push edi ; code goes in here pop edi pop esi pop ebx ret TestFunction endp
本当ならば、使用する予定のレジスタの値だけを保存しておけばよいのですが、ここではpush
命令とpop
命令の使い方を示すためにこうしました。
ここで注目してほしいのは、関数を終了するときのスタックの状態が、関数に入った時点のものと同じでなければならないということです。言い換えると、push
文を記述した場合は、その関数が戻る前に、それぞれのpush
文に対応するpop
文を記述しなければなりません。
フラグとフラグに関連する命令
フラグとは、プロセッサ内でtrue
またはfalse
の値を持つことができる設定です。プロセッサには、さまざまな処理の終了状態を表す一連のフラグが含まれています。さまざまなフラグがありますが、本稿では「ゼロフラグ」を取り上げることにします。このフラグは、ある種の処理でレジスタがゼロになったことを示すためにセットされます。その他の処理では、等価であることを表すためにこのフラグがセットされます。
たとえば、減分を行うdec
命令を考えてみましょう。この命令は、指定のレジスタまたは値を1ずつ減じます。結果がゼロになった場合は、ゼロフラグがセットされます。次に例を示します。
TestFunction proc mov eax, 2 dec eax ; eax == 1 dec eax ; eax == 0, zero flag is set ret TestFunction endp
その他に、特定のフラグの状態に応じて動作が変わる処理もあります。そうした処理の一例がジャンプです。最も基本的なjmp
命令では、プログラムの実行をメモリ内の指定の場所にジャンプさせます(ジャンプ先は、通常はラベルで指定します。これはC++のgoto
文に似ています)。ジャンプにはさまざまな形式があり、たとえばjz
命令(ゼロの場合にジャンプ)、jnz
命令(ゼロでない場合にジャンプ)などがあります。
これらの命令とゼロフラグに関する知識を使用すれば、ループを記述することができます。
LoopFunction proc xor eax, eax ; efficient way of saying eax = 0 mov ecx, 5 ; ecx is the register generally used for counters LoopStart: ; this is a label, used for labelling code positions inc eax dec ecx jnz LoopStart ; eax now equals 5 ret LoopFunction endp
まとめ
本稿では、アセンブラの基本的な命令をいくつか紹介し、その使い方を説明しました。また、レジスタとは何かと、アセンブラ内に存在するレジスタについても解説しました。さらに、アセンブラでパラメータ付きの関数を定義する方法と、アセンブラ関数をC++内で宣言する方法を示しました。
次の回では、算術演算と、アセンブラコードの開発を容易にするMASMのマクロについて説明したいと思います。