このチュートリアルでは、DLLがどういうもので、どうやれば作成できるかについての説明を行う。
DLLソース | 定義ファイル |
DLL実行ソース1 | 実行結果1 |
DLL実行ソース2 | 実行結果2 |
|
プログラミングの経験が長くなればわかると思うが、たいてい、同じ処理が何回も必要になってくる。 その同じ処理をそのつど書き直していたら非常に無駄なので、 DOS時代のプログラマ達はその共通処理部分をスタティックリンクライブラリ(LIB)に収めていた。 その共通部分の関数が使用したければ、リンクするときにLIBからその関数部分をリンカに抽出してもらい、 作成する実行ファイルに埋め込んでもらう。この方法は「静的リンク」と呼ぶ。 Cのランタイムライブラリが良い例だ。
ただし、この方法の欠点は、全く同じコードがそれぞれの実行ファイルに存在することになるので、 ディスクスペースがもったいないことである。 しかしDOSプログラムでは、(シングルタスクOSなので)メモリ上で動作するプログラムは1つしかないので、 メモリを無駄に使うことはなく、この方法でも全然受け入れられた。
ところがWindowsではマルチタスクOSとなったので状況が変わり、 非常に多くのプログラムを実行させるとすぐにメモリを食いつぶしてしまう。 なので、ダイナミックリンクライブラリ(DLL)を使用することによりこの問題に対処した。 DLLとは関数の集まりという点ではLIBと同じだが、 そのDLLの関数を使用するプログラムが同時にいくつあったとしても、 1つしかメモリにロードされないというところが違う。
この点をもう少し深く説明しよう。 同じDLLを使用するプロセスは全て、そのDLLのコピーを自分のプロセスのメモリ空間に保持している。 なので、メモリ上にそのDLLをいくつもコピーしているように思えるのだが、 Windowsがページングというマジックを使い、 全てのプログラムは同じDLLを共有しているのである。 そのため、物理的なメモリという意味においては、DLLのコピーは1箇所しかない。 しかしながら、個々のプロセス毎にDLLを使用する際に必要となるデータセクションがある。
旧来のLIBとは違って、DLLは実行時にリンクを行うので、動的リンクライブラリと呼ばれる。 必要なければDLLを実行時に取り外すこともできる。 それにより、そのDLLを使用しているプログラムが無くなれば、即座にメモリからアンロードされるが、 他にそのDLLを使用しているプログラムがあれば、メモリにまだ残ることになる。
一見簡単そうに思えるが、実はリンカが非常によく働いているからだ。 リンカが実行ファイルを作成するときにDLLのアドレス空間を決定しなければならないからだ。 なぜそのアドレス空間の決定が難しいかというと、 DLLから関数部分を抽出して実行ファイルを作成できないので、 どうにかして実行時にDLLから関数部分を抽出して、 実行ファイルのアドレス空間に配置しなければならないのだが、 そのDLLの情報を実行ファイルに埋め込まなければならないのだ。
その情報というのがインポートライブラリに入っている。 リンカはそのインポートライブラリから必要な情報を抽出し、実行ファイルに「情報」を埋め込む。 そのため、Winodwsローダがプログラムをメモリにロードしたときに、 そのプログラムがDLLをリンクしていることがわかるので、 そのDLLを探し、DLL内の関数を使用できるようにアドレス空間をマッピングする。
また、Windowsローダの手助けを借りずに自分でDLLをロードする方法もあるのだが、 その方法の是非は以下のとおりだ。
- インポートライブラリは必要ないので、インポートライブラリの情報なしにDLLを使用することができる。ただし、DLLの中の関数について、どんな引数をとるかとか、どのような関数名だとかなどを、あなたが知っていなければならない。
- ローダにプログラムをロードしてもらうとき、もしそのプログラムが使用するDLLが見つからなかったら、「指定されたxxxxx.dllは見つかりませんでした」というエラーメッセージが表示され、たとえそのDLLが無くても動作に支障がなくても強制的に終了してしまう。もし自分でDLLをロードする場合、もし見つからなくても、支障が無ければユーザにひとまずそのDLLが無いことを伝え処理を続けることができる。
- 関数についての情報が十分ある場合、インポートライブラリに含まれていない関数を呼ぶことができる。
- もしLoadLibrary関数を使用するのなら、関数を呼び出す度毎にGetProcAdress関数をCALLしなければいけない。GetProcAddress関数はDLL内関数のエントリポイントを取得する。そのため、プログラムが若干ではあるが大きくなり、遅くなるが、気にするほどのことではない。
LoadLibrary関数をCALLすることによるメリット、デメリットを知ったなら、今度はどうやってDLLを作成するかを説明しよう。 DLLを作成するスケルトンコードは次のようになる。
;-------------------------------------------------------------------------------------- ; DLLSkeleton.asm ;-------------------------------------------------------------------------------------- .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib .data .code DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax,TRUE ret DllEntry Endp ;--------------------------------------------------------------------------------------------------- ; これはダミーの関数で、何もしない ; ただ単に、DLLに関数を格納するにはどこに書けばいいのかがわかるために書いている ;---------------------------------------------------------------------------------------------------- TestFunction proc ret TestFunction endp End DllEntry ;------------------------------------------------------------------------------------- ; DLLSkeleton.def ;------------------------------------------------------------------------------------- LIBRARY DLLSkeleton EXPORTS TestFunction全てのDLLはエントリポイント関数を持たなければならない。 Windowsはエントリポイント関数を、
- DLLがまずロードされる
- そしてDLLがアンロードされる
- スレッドが同じプロセスで作成される
- そしてスレッドが破棄される
時に呼び出すようになっている(たいていは初期化のような役割)。
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax,TRUE ret DllEntry Endpエントリポイント関数名は END <Entrypoint function name> とマッチしていれば、どんな名前でも使用できる。この関数は3つの引数を取り、最初の2つの引数だけが重要である。
- hInstDLLはDLLのモジュールハンドルである。プロセスのインスタンスハンドルとは異なり、後で使うことがあるのならこの値をどこかに保存しておかなければならない。そう簡単にはもう一度取得できないので注意すること。
- reasonは以下の4つのうちどれかを指定する。
- DLL_PROCESS_ATTACH
一番最初にプロセスのアドレス空間に格納されたとき、DLLはこの値を受け取ることになっている。この機会に初期化を行える。- DLL_PROCESS_DETACH
プロセスのアドレス空間からアンロードされたときに、DLLがこの値を受け取る。このときにメモリを解放するなどのクリーンアップコードを実行する。- DLL_THREAD_ATTACH
プロセスが新しくスレッドを作成したときに受け取る。- DLL_THREAD_DETACH
プロセス内のスレッドが破棄されたときに受け取る。DLLに処理を続行させたければ、eaxレジスタにTRUEを代入してリターンすればよい。FALSEでリターンしたら、DLLはロードされないだろう。例えば、初期化コードはどこかにメモリに確保されるがきちんと実行されず、エントリポイント関数はDLLが実行できなかったことを示すためにFALSEを返すだろう。
DLLに格納したい関数はエントリポイント関数の前でも後ろでも書くことができるのだが、他のプログラムから呼べるようにしたければ、モジュール定義ファイル(.defファイル)のエクスポートリスト(外部プログラムが使用できる関数リスト)にそれらの関数名をリストアップしなければならない。
DLLの開発過程において、モジュールファイルが必要になってくる。それは次のようになっている。
LIBRARY DLLSkeleton EXPORTS TestFunction最初の行のLIBRARYはDLLの内部モジュール名を定義するもので、DLLファイル名と一致すべきだ。
2行目のEXPORTSはリンカにDLL内のどの関数がエクスポートされているかを教える。つまり他のプログラムから呼び出せるのである。この例では、TestFunctionという関数を他のプログラムからCALLしたいため、EXPORTSにその関数名を記述したのである。
link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def/LIBPATH:c:\masm32\lib DLLSkeleton.objアセンブラのオプションは同じで、/c /coff /Cp だが、そのオブジェクトファイルをリンクしたら .dll ファイルと .lib ファイルができる。 .libファイルは、DLL内の関数を使用したい他のプログラムとリンクするために使用するインポートライブラリである。
;--------------------------------------------------------------------------------------------- ; UseDLL.asm ;---------------------------------------------------------------------------------------------- .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib .data LibName db "DLLSkeleton.dll",0 FunctionName db "TestHello",0 DllNotFound db "Cannot load library",0 AppName db "Load Library",0 FunctionNotFound db "TestHello function not found",0 .data? hLib dd ? ; the handle of the library (DLL) TestHelloAddr dd ? ; the address of the TestHello function .code start: invoke LoadLibrary,addr LibName ;--------------------------------------------------------------------------------------------------------- ; ロードしたいDLLファイル名を指定してLoadLibrary関数をCALLする。もし成功すれば ; DLLのハンドルが返ってくるが、失敗すればNULLが返る。 ; そのDLLのハンドルをGetProcAddress関数や他のハンドルを必要とする関数の引数に渡して ; DLLに関する色々な処理を行える ;------------------------------------------------------------------------------------------------------------ .if eax==NULL invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK .else mov hLib,eax invoke GetProcAddress,hLib,addr FunctionName ;------------------------------------------------------------------------------------------------------------- ; DLLのハンドルを取得したので、DLL内の呼び出したい関数へのポインタを取得するため、 ; 今取得したDLLのハンドルとその関数名とを引数にしてGetProcAddress関数をCALLする。 ; 成功すればその関数へのポインタが返ってくるのだが、失敗すればNULLが返る。 ; DLLをアンロードやリロードしなければそのアドレスは変更しないので、 ; 将来のためにそのアドレスをグローバル変数に格納する ;------------------------------------------------------------------------------------------------------------- .if eax==NULL invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK .else mov TestHelloAddr,eax call [TestHelloAddr] ;------------------------------------------------------------------------------------------------------------- ; 次に、引数に関数へのポインタを指定し、関数をCALLする(関数へのポインタだからといって特別な違いは無い) ;------------------------------------------------------------------------------------------------------------- .endif invoke FreeLibrary,hLib ;------------------------------------------------------------------------------------------------------------- ; DLLが必要なければ、FreeLibrary関数によりアンロードできる ;------------------------------------------------------------------------------------------------------------- .endif invoke ExitProcess,NULL end start見てわかるように、LoadLibrary関数を使用するのは少々複雑にはなるが、より柔軟にもなる。