CodeZine(コードジン)

特集ページ一覧

MASM32によるアセンブラ入門:パート2

アセンブラ言語とその命令

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/04/19 00:00

シリーズ第1回では、Microsoft Assemblerを使用してアセンブラファイルをコンパイルできるようVisual Studioをセットアップする方法を説明しました。第2回では、アセンブラという言語そのものと、アセンブラに含まれているいくつかの命令について解説したいと思います。

お読みになる前に

 本稿の内容は、読者の環境に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 - メモリ操作用のデスティネーションレジスタ

 レジスタeaxebxecxedxは、参照方法を変更することで、そのレジスタを構成するバイトへと分割することができます。たとえば、アキュムレータレジスタ(Aレジスタとも呼ばれます)の場合は次のようになります。

  • al: eaxレジスタ内の下位ワードの下位(lower)バイト
  • ah: eaxレジスタ内の下位ワードの上位(higher)バイト
  • ax: eaxレジスタの下位ワード(つまり2バイト)
  • 使用例:(ah << 8) + al
  • eax: レジスタ全体 (4バイト)

 ebxecxedxに対しても、次の図のように同様の命名規則が適用されます(esiおよびediには当てはまりません)。

 これらの名前は、プロセッサの発展経緯に拠ります。レジスタ名の先頭の「e」は「拡張(extended)レジスタ」を表しています。つまり、16ビットプロセッサから32ビットプロセッサに移行したときの、それぞれのレジスタの32ビット版という意味です。16ビットプロセッサで使用できるレジスタはaxbxcxdxなどだけでした。32ビットプロセッサが登場したときに、新たに使用可能になった16ビット分を「e」という接頭辞で表すようになりました。ただし、レジスタの上位16ビットに直接アクセスする方法はありません。

mov(移動)命令

 まず、最も単純なmov(移動)命令から見ていくことにしましょう。mov命令は、プロセッサ内部で値を移動する方法を表します。次に例を示します。

mov eax, 100

 この命令では、100という値をeaxレジスタに「移動」しています。これはeax=100と同じことです。mov命令の構文は次のようになります。

mov (destination), (source)

 sourcedestinationは同じサイズ(ビット数)でなければなりません。次に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>という形式で必要な数だけ指定します。パラメータの型として使用できるのはDWORDWORDBYTEです。

 関数の終わりには、関数名とendpを含む行を記述します。

 ret文は戻り値を返す文です。言い換えれば、この文のところで関数が終了します。ret文は関数の最後に記述しなければなりません。

 この関数をC++から呼び出す場合は、関数から戻る前に、レジスタebxesiediの元の値を復元する必要があります。これを行うには、通常はpushpopを使用します(詳しくは後述)。

 関数の戻り値はeaxレジスタに格納されます。ほとんどの命令では、関数のパラメータに名前でアクセスすることができます。次に例を示します。

TestProc proc dwValue1:DWORD, dwValue2:DWORD

   mov eax, dwValue1
   add eax, dwValue2
   ret

TestProc endp

 この関数では、dwValue1dwValue2を加算して、結果を返します。

 C++からアセンブラ関数にアクセスするには、同じ名前とパラメータを持つ関数を宣言する必要があります。C++でのパラメータのサイズは、アセンブラコード内で定義したパラメータのサイズに等しくなければなりません。さらに、extern "C"として宣言し、stdcall呼び出し規則を使用する必要があります。たとえば上記のアセンブラ関数に対するC++宣言は次のようになります。

extern "C" unsigned int __stdcall TestProc(unsigned int dwValue1,
                                           unsigned int dwValue2);

 ポインタを渡す場合は、そのポインタをアセンブラ関数のDWORDパラメータとして宣言します(32ビットオペレーティングシステムではポインタのサイズが32ビットなので)。同様に、charBYTEとして渡し、WCHARWORDとして渡すようにします。

 アセンブラ関数を静的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のマクロについて説明したいと思います。

 パート3へ→



  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • darwen(darwen)

    Windows 3.11のVisual C++/MFC 1.5の時代からWindowsプログラミングに取り組み始める。現在は、多言語展開を図っているとあるソフトウェア会社のベテラン開発者としてあらゆるプロジェクトに従事。暇さえあればプログラミングをしているのではないかという噂あり。

  • japan.internet.com(ジャパンインターネットコム)

    japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.com や EarthWeb.c...

バックナンバー

連載:japan.internet.com翻訳記事

もっと読む

All contents copyright © 2005-2020 Shoeisha Co., Ltd. All rights reserved. ver.1.5