はじめに
たいていのプログラマは、アセンブラ(またはアセンブリ言語)と聞くと尻込みをします。一般にアセンブラは、学ぶのも使用するのも非常に難しい言語だと思われています。さらに、アセンブラの使い方を知っている人は、周囲のプログラマからある種の尊敬を集める傾向にあります。
このチュートリアルは、アセンブラに対するこのような誤解を払拭するための3回シリーズのうちの1回目です。このチュートリアルを通して、アセンブラは本当は難しい言語ではなく、むしろ簡単な言語であることを証明したいと思います。
第1回目では、アセンブラのコーディングを大幅に単純化するためのツールを紹介し、これをVisual Studioに統合する方法を説明します。
アセンブラとは
そもそも、アセンブラとは一体何でしょうか。ごく簡単に言えば、アセンブラはプロセッサの言語です。これより低いレベルに下がることはできません(各命令の実際のバイト値を操作する場合は話が別ですが)。mov
、add
などのコマンドはすべてアセンブラプログラムによって数値に直接変換され、それがプロセッサに渡されて実行されます。
アセンブラが他の言語より優れているのは、処理速度の速さです。アセンブラではプロセッサの実行速度をそっくりそのまま利用できるのです。最新のコンパイラを使用してコードを最適化したとしても、そのコードの実行速度は、アセンブラで記述して最適化した同等のコードにはかなわないでしょう。
しかし、アセンブラは何にでも使用できるわけではありません。1つのアプリケーションを全部アセンブラで記述することも可能ですが、現在ではC++などの高等言語を利用できるため、そんなことをするのは物好きくらいのものです。大多数のアプリケーションでは、C++の速度、あるいは.NETの速度でも十分に許容範囲内です。
アセンブラが利用されるのは、速度が重視されるアプリケーション、たとえばグラフィックアプリケーションなどにおいてです。大きなメモリブロックを処理する小さくて非常に高速な関数を記述する場合は、アセンブラにかなうものはありません。ビットマップ操作は、アセンブラの知識が大きく物をいうケースの代表例です。
アセンブラという言語の概要と長所を理解したところで、次はその使い方を見ていきましょう。
Visual C++内でアセンブラを使用する
C++内でアセンブラを記述する1つの方法は、__asm
ブロックを使用することです。
DWORD Function(DWORD dwValue)
{
__asm
{
mov eax, dwValue
add eax, 100
mov dwValue, eax
}
return dwValue;
}
この例では、__asm
ブロック内にアセンブラの命令をいくつか記述しました。C++コンパイラはこれらの命令をそれぞれのマシン語コードに直接変換します。
コードの意味は考えなくてかまいません。add
が値の加算命令で、mov
が値の移動命令であるということがわかれば十分です。上記のコードでは、渡された値に100を加算しているだけです。
なんだそれだけのことか、と思った人もいるのではないでしょうか。しかし、それは間違いです。__asm
ブロック内にコードを記述するという方法は、短いアセンブラコードの場合は問題ないのですが、ある程度長い関数を記述しようとすると少々面倒なことになります。たとえば、if..else
文は次のようになります。
DWORD Function2(DWORD dwValue1, DWORD dwValue2) { DWORD dwValue3 = 0; #if 0 // this is the Assembler if (dwValue1 == dwValue2) { dwValue3 = 1; } else { dwValue3 = 2; } #endif __asm { mov eax, dwValue1 ; this is the test of the values, i.e. if dwValue1 == dwValue2 cmp eax, dwValue2 ; jump to 'Else' if they are not equal jne Else mov eax, 1 jmp EndIf Else: mov eax, 2 EndIf: mov dwValue3, eax } return dwValue3; }
ここでも、実際の命令についてはあまり気にしないでください。しかし、すべてのif
文についてこのようなコードを書かなければならないとしたらどうでしょうか。
__asm
コードブロックを使用するのは、メモ帳とコマンドプロンプトを使用してC++を記述するようなものです。確かにできなくはありませんが、専用ツールを使用した場合よりもはるかに時間がかかります。
MASM32
では、アセンブラを記述するための専用ツールにはどんなものがあるのでしょうか。ここでは、Microsoft Macro Assemblerを紹介しておきます(聞き間違いではありません。Microsoftもアセンブラを提供しているのです)。実はMicrosoftは1980年からアセンブラを提供しているのですが、あまり知られてはいません。さらに重要なことに、Microsoft Macro AssemblerはVisual C++リンカと互換性のある.objファイルを生成します。デバッグ情報を含んだ.objファイルを生成し、コードをステップ実行することも可能です。
この記事では、MASMを使うためのフリーウェアである「MASM32」というアセンブラを使用します。MASM32はhttp://www.masm32.com/から入手できます。先に進む前に、MASM32をダウンロードしてインストールしてください。
MASM32にはどんなメリットがあるのでしょうか。
第一に、膨大な量のヘルプファイルが付属しています(「\msasm32\help」ディレクトリに収録)。
第二に、開発者が必要とするであろう各種のジョブについてのマクロが用意されています。さらに、これらのマクロを使用すれば、それぞれのタスクを実行する最も効率的な方法を選択していることが確信できます。
たとえば、前述のコードをMASM32を使用して記述すると次のようになります。
Function proc dwValue1:DWORD, dwValue2:DWORD mov eax, dwValue1 .if eax == dwValue2 mov eax, 1 .else mov eax, 2 .endif ret Function endp
この方がずっと読みやすいはずです。
MASM32をダウンロードしてインストールしたところで、以降では、アセンブラファイルを既存のC++プロジェクトに追加する方法と、それをコンパイルするためのプロジェクト設定の設定方法、アセンブラで記述した関数にC++からアクセスする方法について、順を追って見ていきたいと思います。
アセンブラファイルをVisual C++に追加する
以降の手順はVisual Studio.NET 2002(英語版)のものですが、Visual C++ 6またはVisual Studio.NET 2003でも同様に実行できます。
まず、アプリケーションの検索ディレクトリにMASM32の「bin」フォルダを含める必要があります。[Tools]→[Options]→[Projects]→[Visual C++ Directories]を選択します。MASMをインストールしたパスの下にある「bin」フォルダを追加します。たとえば、「C:\MASM32」ディレクトリにインストールした場合は「C:\MASM32\bin」となります。
このディレクトリをリストの一番下に移動します。これが非常に重要です。このフォルダには「link.exe」というリンカアプリケーションも含まれており、このリンカがVisual Studioで既定のリンカの代わりに使用される可能性があるからです。
次にコンソールアプリケーション用のプロジェクトを作成します。
作成したら、「test.asm」という名前のファイルをプロジェクトに追加します。ソリューションエクスプローラ内の「Source Files」フォルダを右クリックし、[Add]→[Add New Item]を選択します。
「test.asm」と入力して[Enter]キーを押すと、「test.asm」という新規ファイルが開かれます。このファイルに次のコードを入力します。
.486 .model flat, stdcall option casemap :none .code TestProc proc dwValue:DWORD mov eax, dwValue add eax, 100 ret TestProc endp end
このコードでは、入力値(dwValue
)に100を加算し、結果を返しています。
end
を必ず入れます。さらに、.code
行とend
行の間にコードを記述する必要があります。次は、このファイルに関するビルドプロパティを設定する必要があります。デバッグビルドの状態であることを確認し、ソリューションエクスプローラ内でこのファイルを右クリックし、[Properties]を選択します。[Custom Build Step]→[General]を選択し、[Command Line]に次のコマンドラインを入力します。
ml /c /coff /Zi /Fo"$(OutDir)\$(InputName).asm.obj" "$(InputFileName)"
さらに、[Outputs]に次のパスを入力します。
$(OutDir)\$(InputName).asm.obj
設定は次のようになります。
リリースビルドの設定はこれとほとんど同じですが、コマンドラインに「/Zi」オプションを含めない点だけが異なります。これはデバッグ情報を生成するためのオプションなので、リリースビルドでは削除します。従って次のようになります。
ml /c /coff /Fo"$(OutDir)\$(InputName).asm.obj" "$(InputFileName)"
もちろん、これをマクロやアドインにして、このプロセスを自動化することもできます。ここでは、現在使われているどのバージョンのVisual Studioにも応用できるように、手順を1つ1つ紹介しておきました。
次は、アセンブリで書かれた関数をC++から呼び出す方法を説明します。
C++からアセンブラコードを呼び出す
上記のアセンブラコードでは、DWORD
(つまり32ビット値)の入力を受け取るTestProc
という関数を定義していました。この関数を呼び出すためには、C++内で関数を宣言する必要があります。アプリケーションのmain
メソッドが記述されている.cppファイル内で、先頭の#includes
の後に次の行を入力します。
extern "C" unsigned int __stdcall TestProc(unsigned int dwValue);
さらに、main
メソッド内に次のコードを記述します。
int main(int argc, _TCHAR* argv[]) { unsigned int dwValue = 100; unsigned int dwReturn = TestProc(dwValue); printf("%d\n", dwReturn); getchar(); return 0; }
このアプリケーションを実行すると、予想どおり、「200」という結果が返されます。さらに、TestProc
を呼び出す行にブレークポイントを設定していた場合は、このメソッドにステップインし([F11]キー)、アセンブラコードをステップ実行することができます。実は、アセンブラコード内にブレークポイントを設定することも可能です。
まとめ
第1回では、アセンブラファイルを作成し、コンパイルし、C++コードから呼び出す方法について説明しました。シリーズ第2回では、アセンブリ言語を構成している実際の命令について説明し、レジスタなどのトピックについても取り上げることにします。