Tutorial 17: Dynamic Link Libraries

このチュートリアルでは、DLLがどういうもので、どうやれば作成できるかについての説明を行う。
 DLLソース   定義ファイル 
 DLL実行ソース1   実行結果1 
 DLL実行ソース2   実行結果2 

Theory:

プログラミングの経験が長くなればわかると思うが、たいてい、同じ処理が何回も必要になってくる。 その同じ処理をそのつど書き直していたら非常に無駄なので、 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をロードする方法もあるのだが、 その方法の是非は以下のとおりだ。

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はエントリポイント関数を、

時に呼び出すようになっている(たいていは初期化のような役割)。

DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
       mov eax,TRUE
       ret
DllEntry Endp

エントリポイント関数名は END <Entrypoint function name> とマッチしていれば、どんな名前でも使用できる。この関数は3つの引数を取り、最初の2つの引数だけが重要である。

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関数を使用するのは少々複雑にはなるが、より柔軟にもなる。


[戻る]