Tutorial 2: MessageBox

このチュートリアルでは、"Win32 assembly is great!"という文字が入ったメッセージボックスを 表示させるウィンドウズプログラムを作成する。
   ソース      実行結果  

Theory:

ウィンドウズにはウィンドウズプログラムを作成するための豊富な資源が準備されている。 その中核をなすものが、ウィンドウズAPI(Application Programming Interface)だ。 ウィンドウズAPIはウィンドウズ自身に備わっているとても便利な関数群のことで、 どんなウィンドウズプログラムからでもこれらのAPIを使用することができる。 これらの関数群の実体はkernel32.dll, user32.dll and gdi32.dllといったDLLの中にある。 Kernel32.dllはメモリやプロセスを管理する関数郡が収められており、 User32.dllはユーザーインターフェイスをつかさどっている。 Gdi32.dllは描画処理を行っている。 これらのDLL(のことをまとめて、"Big 3" とか "main 3"と言われている)以外ももちろん使用可能であり、 API関数についてさらに詳しく解説していこう。

ウィンドウズのプログラムはこれらのDLLを実行時にリンクするようになっている。 つまり、実行ファイルにAPI関数のコードは含まれないのである。 そのためプログラムには、実行時に要求したAPI関数がどこにあるか見つけるために必要な情報を 書かないといけない。 その情報はインポートライブラリにあり、プログラムを作成する時には、 正しいインポートライブラリをリンクさせないといけない。 さもないと、API関数の場所がわからないのだ。

ウィンドウズプログラムはメモリにロードされる時、 ウィンドウズはそのプログラムに格納された情報を読み取る。 その情報というのは、そのプログラムが使用する関数名と その関数が組み込まれているDLLの名前である。 ウィンドウズがそのような情報を見つけると、 記述されているDLLをロードし、必要な関数のアドレスがセットされ、 関数を呼び出せるようになる。

API関数には2つのカテゴリがある。 1つはANSIでもう1つはUnicodeである。 ANSI用のAPI関数名の接尾辞は"A"となっている。例えば、MessageBoxA のように。 対して、Unicode用のAPI関数名の接尾辞は"W"となっている(これは Wide Char の"W"であろう)。 Windows95はANSIがデフォルトで、WindowsNTはUnicodeとなっている。

われわれは通常、NULL文字('\0')で終わるANSI文字配列を使用する。 ちなみにANSI文字のサイズは1バイトである。 そのANSI文字コードはヨーロッパの言語も差し支えないのだが、 アジア系の言語は数千の単語(漢字)があるので、扱えないのである。 そのため、UNICODEが考え出された。 UNICODEのサイズは2バイトで、65,536文字を識別できるようになる。

しかし、プラットフォームによってどちらのAPI関数を使用するかを選択したいことがよくある。 そんなときは、接尾辞の無い("W"も"A"も付かない)API関数を呼べばよい。

Example:

では、スケルトンプログラムをお見せしよう。 このプログラムをベースにして後でメッセージボックスを出力するようにします。

.386
.model flat, stdcall
.data
.code
start:
end start

プログラムの実行は、end の後ろにあるラベル名の直下の命令から開始される。 このスケルトンプログラムでは、startラベルのすぐつぎのプログラムから始まることになる。 そして、フロー制御命令に出会うまで実行されていく。 ちなみに、フロー制御命令とは、 jmpjnejeret、 といった命令群だ。 これらのフロー制御命令は、文字通りプログラムの流れを変更させるものだ。 ウィンドウズプログラムは終了する時に、ExitProcess というAPI関数を呼び出す必要がある。

ExitProcess proto uExitCode:DWORD

上の行は関数のプロトタイプだ。 関数プロトタイプは、アセンブラやリンカにその関数の性質を教える役目を持っており、 型チェックが(アセンブラやリンカが自動的に)行えるようになる。 関数プロトタイプのフォーマットはこのようなものだ。

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...

簡単に説明すると、PROTOキーワードに続く関数名があり、その関数が必要とする引数のリストを カンマで区切って記述すればよい。先の例におけるExitProcessでは、 唯一の引数 DWORD を受け取る関数として定義されている。
関数プロトタイプは、高レベル関数呼び出し構文を使用する際に非常に便利なのだ。 型チェック付きの関数を単純に呼び出す(Call 構文を使って)ことを考えてみよう。 例えば、

call ExitProcess

として、ExitProcessを呼び出したとき、dword をスタックにプッシュしていなかったらどうなるだろうか? アセンブラやリンカはエラーを検出できないので、プログラムがクラッシュした後、 あなたは自分のミスに気付くだろう。しかし、

invoke ExitProcess

とした場合は、リンカがあなたにスタックにプッシュしていないことを教えてくれて、 エラーを回避できるだろう。 なので、call構文ではなくこのinvoke構文を使うようお勧めする。 invoke構文の書き方は以下の通りだ

INVOK expression [,arguments]

expression には、関数の名前か関数へのポインタを記述できる。 関数の引数はコンマ区切りとなっていなければならない。

ほとんどのAPI関数のプロトタイプはインクルードファイルに収められている。 もし hutch のMASM32を使用したなら、MASM32/include フォルダにそのインクルードファイルがある。 そのインクルードファイルは、.inc という拡張子となっており、 あるDLLに入っている関数は、そのDLLの名前.inc に関数プロトタイプも入っている。 例えば、ExitProcess 関数は kernel32.lib でエクスポートされているので、 ExitProcess 関数のプロトタイプは kernel32.inc に入っていることになる。
また、自分の関数のプロトタイプを作ることもできる。
私の例は全体にわたって、hutchの windows.inc(http://win32asm.cjb.net からダウンロードできる) を使用している。

ExitProcess 関数に話を戻すと、 uExitCode 引数はWindowsへの戻り値である。そのため、ExitProcess関数は次のようにCALLできる。

invoke ExitProcess,0

この行をスケルトンプログラムの start ラベルのすぐ下に記述してみよう。 これで、すぐに終了するWin32プログラムを書くことができた。 もちろん、依然このプログラムは正常なままだ。

.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
start:
        invoke ExitProcess,0
end start

casemap オプションは、文字列の大文字小文字を区別させるようにするもので、 このオプションを使用すると、ExitProcess と exitprocess は別物になる。 次に新しい命令、include について説明する。 これは、include に続いて記述されるファイル名を、その include 命令が記述された場所に、 挿入するものである。 上の例では、include \masm32\include\windows.inc という行に達した時に、そこに windows.inc の内容を書き写したかのように、 windows.inc ファイルを開き、windows.inc に記述されている内容が実行される。 hutchのwindows.inc には、win32に必要な定数や造体が定義されているが、 関数プロトタイプは無い。windows.inc は決して理解しやすいものではない。 hutch と私はできるかぎり定数や構造体を windows.inc に記述したのだが、 まだ漏れが有るため、引き続きアップデートしていく予定なので、 hutch や私のホームページをチェックしてもらいたい。
windows.in から、プログラムは定数や構造体を取得できたので、 今度は関数プロトタイプなのだが、 これは違うファイルを include しないといけない。そのファイルは \masm32\include ディレクトリにある。

の例では、kernel32.dll でエクスポートされた関数をCALLしたので、 kernel32.dll の関数プロトタイプを持つファイル(kernel32.inc)をインクルードしないといけない。 テキストエディタで kernel32.inc を開くと、kernel32.dll で使用する全関数のプロトタイプ を確認できるだろう。もしkernel32.inc をインクルードしなければ、 ExitProcess 関数をCALLすることはできるのだが、単純構文である call 命令でしかCALLできない。 invoke 命令では関数をCALLできないのだ。 注意する点は、invoke 命令で関数呼び出しをするには、 ソースファイルのどこかに呼び出したい関数のプロトタイプを記述しないといけない、ということだ。 上の例では、kernel32.inc をインクルードしなければ、 ExitProcess関数 を invoke命令で呼び出す前に、ExitProcess関数のプロトタイプを どこかに記述すれば動くようになる。
The include files are there to save you the work of typing out the prototypes yourself so use them whenever you can.
今度もまた新しい命令includelibについて説明しよう。 似てはいるが、include とは働きが違い、 インポートライブラリをアセンブラに教える命令だ。 アセンブラがこの命令を見つけると、オブジェクトファイルに リンクコマンドを埋め込み、リンカがどのファイルをリンクするのかわかるようにする。 includelib 命令を使わなくても、コマンドラインからリンカを起動する際に インポートするファイル名を与えてやれば、同じことができるのだが、 コマンドラインからの方法は、128文字という制限もあるし、そもそもめんどくさいし、 退屈なので、includelib 命令を使った方が絶対よい。

この例を、msgbox.asm というファイル名で保存する。 そして、ml.exe にパスが通っているものとして、msgbox.asm をアセンブルする。

ml /c /coff /Cp msgbox.asm

msgbox.asm のアセンブルが成功すると、msgbox.obj ファイルが作成される。 msgbox.obj ファイルというのはオブジェクトファイルのことで、実行ファイルの1段階前の状態だ。 オブジェクトファイルは命令とデータをバイナリ形式で持っており、 足りないのは、リンカによってアドレスが固定されることだけだ。

では次にリンクを行う

link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj

リンカはオブジェクトファイルを読み取り、インポートライブラリから取得したアドレスを埋め込む。 これが終われば、msgbox.exe が作られる。

その msgbox.exe を実行してみよう。たぶん何もおこらないはずだ。 でもそれは間違いなくウィンドウズのプログラムなのである。 そして、そのサイズを見てごらん。私のだと、たったの1,536バイトしかないんだよ。

さー、では message box を出してみよう。message box を出す関数は次のとおりだ

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

さーでは、msgbox.asm をいじっていこう。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText      db "Win32 Assembly is Great!",0

.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start

これをアセンブルして実行してみよう。 "Win32 Assemblyis Great!" というテキスト文字列を持った message box が出てくるだろう。

もう一度ソースコードを見てみよう

ソースには、.data セクションに NULL文字で終了する2つの文字列が定義されている。 ANSI で扱う全ての文字列は NULL で終了しなければならないということを覚えておこう。

そして、2つの定数 NULL と MB_OK を使用している。 これらの定数については windows.inc に定義されているので、実際には整数値となる。 これはプログラムの可読性が向上することになる。

引数のところに記述されている、addr演算子は、 変数のアドレスを渡すことになる。この演算子は、invoke 命令でしか有効ではないので、 addr演算子で取得したアドレスを、レジスタや変数に代入することはできない。 たとえば、上の例では addr の代わりに offset を使用することもできるのだが、 addr と offset の間には多少違うところがある。

  1. invoke の行で使用する変数名が、そのinvokeの行より後ろで定義されていた場合、 offset なら動くのだが、addr では動作しない。
  2. invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
    ......
    MsgBoxCaption db "Iczelion Tutorial No.2",0
    MsgBoxText    db "Win32 Assembly is Great!",0

    MASM はエラーを出すだろう。もし、addr の代わりに offset を使用すれば、 MASM は適切にアセンブルしてくれる。

  3. addr はローカル変数でも動作するが、offset は扱えない。 ローカル変数というのは、スタックの、ある未使用領域のどこかに確保されるので、 動作中にしかそのアドレスを知ることができない。 offset はアセンブル時(静的)に動作するものなので、 ローカル変数に使用できないのは、ごく自然だ。 対して、addr はローカル変数でも使用することができる。 というのは、アセンブル時に addr によって参照される変数がグローバル領域に確保されたものか、 ローカル領域に確保されたものかを判断して、動作を変更するからである。 もし、グローバル領域に確保されたものだったとしたら、オブジェクトファイルには、 その変数のアドレスを書き込む。この場合は、offset と同じような動きである。 今度は、ローカル変数のときはどうなるかというと、 関数を呼び出す前に、以下のような変数のアドレスを計算するコードが埋め込まれ、 それらのアドレスを取得した後、関数をCALLする。
  4. lea eax, LocalVar
    push eax

    lea 命令は実行時に変数のアドレスを解決するので、これで期待通りに動作するのである。


[戻る]