LoadLibrary(), GetProcAddress()は面倒くさくて使いたくない、でもDLLのロードは関数が呼ばれるその時まで遅らせたい・・・
そんな要望に応えるのが、VC6よりサポートされたDLLの遅延読み込み(delay loading)である。
呼ぶ側のコードは暗黙的リンク(implicit-link)の書き方のまま、リンカオプションに"/DELAYLOAD:(DLLファイル名)"を付けるだけで自動的にLoadLibrary()+GetProcAddress()を行うスタブが生成される。最初に呼ばれるのはスタブコードだが、LoadLibrary()+GetProcAddress()成功時にはスタブ内部で自動的にIATを書き換えてくれるため、二回目以降は直接DLLを呼ぶようになる(=二度LoadLibrary()が呼ばれることは無い)。
DLLが見つからなかった場合どうする? or スタブの挙動をカスタマイズしたい or 遅延読み込みのタイミングを自分で決めたい・・・なども、フックやヘルパー関数が用意されているため開発者により調整可能となっている。
今回は自作DLLを用意し、遅延読み込みを試してみる。
参考資料:
対象:Visual C++ 2008 Express Edition
> cl Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86 Copyright (C) Microsoft Corporation. All rights reserved. > link Microsoft (R) Incremental Linker Version 9.00.30729.01 Copyright (C) Microsoft Corporation. All rights reserved.
なお動作確認は Windows XP SP3 上で行っている。
遅延読み込みさせるDLLを二つ用意してみた。
dll01.c : funcA1()とfuncB1()をエクスポートする。
dll02.c : funcA2()とfuncB2()をエクスポートする。
dll01.c:
#include <windows.h> int __declspec(dllexport) funcA1(int a, int b) { OutputDebugString("funcA1() start"); return a + b + 1; } int __declspec(dllexport) funcB1(int a, int b) { OutputDebugString("funcB1() start"); return a * b + 1; } BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpvReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: OutputDebugString("dll01, DllMain() : DLL_PROCESS_ATTACH"); break; case DLL_THREAD_ATTACH: OutputDebugString("dll01, DllMain() : DLL_THREAD_ATTACH"); break; case DLL_THREAD_DETACH: OutputDebugString("dll01, DllMain() : DLL_THREAD_DETACH"); break; case DLL_PROCESS_DETACH: OutputDebugString("dll01, DllMain() : DLL_PROCESS_DETACH"); break; default: break; } return TRUE; }
dll02.c:
#include <windows.h> int __declspec(dllexport) funcA2(int a, int b) { OutputDebugString("funcA2() start"); return a + b + 2; } int __declspec(dllexport) funcB2(int a, int b) { OutputDebugString("funcB2() start"); return a * b + 2; } BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpvReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: OutputDebugString("dll02, DllMain() : DLL_PROCESS_ATTACH"); break; case DLL_THREAD_ATTACH: OutputDebugString("dll02, DllMain() : DLL_THREAD_ATTACH"); break; case DLL_THREAD_DETACH: OutputDebugString("dll02, DllMain() : DLL_THREAD_DETACH"); break; case DLL_PROCESS_DETACH: OutputDebugString("dll02, DllMain() : DLL_PROCESS_DETACH"); break; default: break; } return TRUE; }
ビルド:
>cl /LD dll01.c >cl /LD dll02.c
暗黙的リンクの書き方そのままで遅延読み込みが使えることを試す為、ヘルパー関数やエラー処理、フックなどは使わないシンプルなソースを作ってみた:
main01.c:
#include <stdio.h> __declspec(dllimport) int funcA1(int a, int b); __declspec(dllimport) int funcB1(int a, int b); __declspec(dllimport) int funcA2(int a, int b); __declspec(dllimport) int funcB2(int a, int b); int main() { printf("funcA1(2, 3) = %d\n", funcA1(2, 3)); printf("funcB1(2, 3) = %d\n", funcB1(2, 3)); printf("funcA2(2, 3) = %d\n", funcA2(2, 3)); printf("funcB2(2, 3) = %d\n", funcB2(2, 3)); return 0; }
遅延読み込み使うには、"/DELAYLOAD:(DLL名)"をリンカオプションで指定すると共に、SDKで提供されるdelayimp.libをリンクする。
複数のDLLを遅延読み込みさせたい場合は、その分だけ"/DELAYLOAD"を指定すればよい。
> cl main01.c delayimp.lib /link /DELAYLOAD:dll01.dll /DELAYLOAD:dll02.dll ... LINK : warning LNK4199: /DELAYLOAD:dll01.dll は無視されます。dll01.dll にインポートがありません。 LINK : warning LNK4199: /DELAYLOAD:dll02.dll は無視されます。dll02.dll にインポートがありません。 main01.obj : error LNK2019: 未解決の外部シンボル __imp__funcB2 が関数 _main で参照されました。 main01.obj : error LNK2019: 未解決の外部シンボル __imp__funcA2 が関数 _main で参照されました。 main01.obj : error LNK2019: 未解決の外部シンボル __imp__funcB1 が関数 _main で参照されました。 main01.obj : error LNK2019: 未解決の外部シンボル __imp__funcA1 が関数 _main で参照されました。 main01.exe : fatal error LNK1120: 外部参照 4 が未解決です。
遅延読み込み対象のDLLのインポートライブラリも必要だった。
> cl main01.c delayimp.lib dll01.lib dll02.lib \ /link /DELAYLOAD:dll01.dll /DELAYLOAD:dll02.dll
実行:
> main01.exe funcA1(2, 3) = 6 funcB1(2, 3) = 7 funcA2(2, 3) = 7 funcB2(2, 3) = 8
まずヘッダー情報を見てみると、DataDirectoryの"Delay Import Directory"に値がセットされたことを確認出来る。
> dumpbin /headers main01.exe ... 0 [ 0] RVA [size] of Export Directory B86C [ 28] RVA [size] of Import Directory ... 0 [ 0] RVA [size] of Bound Import Directory A000 [ 120] RVA [size] of Import Address Table Directory B7B4 [ 60] RVA [size] of Delay Import Directory ...
続いてインポート情報を見てみると、遅延読み込み対象が専用のフォーマットで出力される:
> dumpbin /imports main01.exe ... Section contains the following delay load imports: dll01.dll 00000001 Characteristics 0040CF60 Address of HMODULE 0040CF40 Import Address Table 0040B814 Import Name Table 0040B854 Bound Import Name Table 00000000 Unload Import Name Table 0 time date stamp 0040108E 0 funcA1 00401073 0 funcB1 dll02.dll 00000001 Characteristics 0040CF64 Address of HMODULE 0040CF4C Import Address Table 0040B820 Import Name Table 0040B860 Bound Import Name Table 00000000 Unload Import Name Table 0 time date stamp 004010B3 0 funcA2 00401098 0 funcB2 ...
興味深いのは、funcA1/B1/A2/B2 のアドレスがmain01.exeモジュール内の絶対アドレスとして既に設定済になっている点。実際にアセンブラレベルでどのように動作するのか、デバッガで追ってみる。
デバッガ(OllyDbg)で追っていくと、起動時は当然dll01.dll/dll02.dllともメモリ上にはロードされていない。
そしてmain()中のfuncA1()を呼ぶところにくると、
0040108E 0 funcA1
の通り 0x40108E をCALLする。実際は
0x401007 : FF15 40CF4000 = CALL DWORD PTR DS:[40CF40]
となっており、0x40CF40は上のインポート情報では遅延読み込みでのdll01.dllのIATアドレスと同じ、つまりIATの先頭を指している。
では0x40CF04には何が最初書き込まれているかというと、それが "0x40108E" になる。
では0x40108Eを見てみると、
0x40108E : B8 40CF4000 = MOV EAX,main01.0040CF40 0x401093 : E9 E5FFFFFF = JMP main01.0040107D
となっている。
0x40108Eの直ぐ上、0x40107DにJMPしてみると、次のようなアセンブラコードになっている。
0x40107D : 51 PUSH ECX 0x40107E : 52 PUSH EDX 0x40107F : 50 PUSH EAX 0x401080 : 68 B4B74000 PUSH main01.0040B7B4 0x401085 : E8 AA880000 CALL main01.00409934 0x40108A : 5A POP EDX 0x40108B : 59 POP ECX 0x40108C : FFE0 JMP EAX
この解説は、参考資料に挙げた"Under the Hood"を参照。(ただしVC6時点で書かれた"Under the Hood"には"PUSH EAX"が出てこない。VC7以降で改良されたのかも知れない)
ともあれ
0x401085 : E8 AA880000 CALL main01.00409934
で、delayimp.lib提供のコードに突入する。
このCALLから戻ってくると、デバッガログにdll01.dllのDllMain()が呼ばれたことを示すOutputDebugString()文字列が表示されていた。
またこの時点でEAXには
0x10001000
つまりdll01.dllのfuncA1のアドレスがセットされている。
これにより、
0x40108C : FFE0 JMP EAX
でfuncA1()へ制御が移る。
さらにこの時点で、0x40CF40には
0x10001000
が書き込まれていた。つまり、IATはfuncA1の実際のアドレスに更新されている。
これにより、次に
FF15 40CF4000 = CALL DWORD PTR DS:[40CF40]
が呼ばれた場合は直接dll01.dllのfuncA1をcallするようになる。
0x40107D - 0x401093までがリンカにより生成された、funcA1に対応するstubコードとなる。
以降、funcB1, dll02.dllのfuncA2, funcB2についても同様となる。stubコードについてはそれぞれの関数毎に生成されていた。
また、funcA1で遅延読み込みが処理された時点では、funcA1のIATは書き換わっていたがfuncB1に相当するIATのアドレスはスタブコードのままだった。つまり、対象DLLの関数どれか一つがロードされた時点で、他の関数のIATまで更新されるわけではない。
dll01.dllをリネームして実行してみると、funcA1()の呼び出しで次の例外が発生する。
アドレス:7C812AFB メッセージ = 例外発生: C06D007E
デバッガ無しで実行すると、「問題が発生したため、main01.exeの実行を~」という一般的な例外発生時のメッセージボックスが表示され、異常終了する。
遅延読み込みでDLLモジュールが見つからない場合は次の例外コードで例外が発生する:
VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND)
関数が見つからない場合は次の例外コードで例外が発生する:
VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND)
構造化例外処理(SEH)のC言語用構文"__try", "__except"を使って例外を補足出来るようにmain01.cを修正してみたのが、以下に示すmain02.cとなる:
#include <windows.h> #include <stdio.h> #include <DelayImp.h> __declspec(dllimport) int funcA1(int a, int b); __declspec(dllimport) int funcB1(int a, int b); __declspec(dllimport) int funcA2(int a, int b); __declspec(dllimport) int funcB2(int a, int b); DWORD MyDelayLoadExFilter(DWORD ec, EXCEPTION_POINTERS *ep) { DelayLoadInfo *dli = NULL; DelayLoadProc *dlp = NULL; if (ec == VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND)) { printf("delay load : ERROR_MOD_NOT_FOUND\n"); } else if (ec == VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND)) { printf("delay load : ERROR_PROC_NOT_FOUND\n"); } else { // 遅延読み込みの例外でない場合は上位の例外ハンドラへ return EXCEPTION_CONTINUE_SEARCH; } // 遅延読み込み用の例外情報を取得 dli = (DelayLoadInfo*)(ep->ExceptionRecord->ExceptionInformation[0]); printf("\tDLL = %s\n", dli->szDll); dlp = &(dli->dlp); if (dlp->fImportByName) { printf("\tProc = %s\n", dlp->szProcName); } else { printf("\tOrdinal = 0x%08x\n", dlp->dwOrdinal); } printf("\tHMODULE = 0x%08x\n", dli->hmodCur); // 処理続行 return EXCEPTION_EXECUTE_HANDLER; } int main() { __try { printf("funcA1(2, 3) = %d\n", funcA1(2, 3)); printf("funcB1(2, 3) = %d\n", funcB1(2, 3)); printf("funcA2(2, 3) = %d\n", funcA2(2, 3)); printf("funcB2(2, 3) = %d\n", funcB2(2, 3)); } __except(MyDelayLoadExFilter(GetExceptionCode(), GetExceptionInformation())) { printf("delay load error.\n"); } return 0; }
ビルド:
> cl main02.c delayimp.lib dll01.lib dll02.lib \ /link /DELAYLOAD:dll01.dll /DELAYLOAD:dll02.dll
正常時:
> main02.exe funcA1(2, 3) = 6 funcB1(2, 3) = 7 funcA2(2, 3) = 7 funcB2(2, 3) = 8
dll01.dllをリネーム時:
> ren dll01.dll _dll01.dll > main02.exe delay load : ERROR_MOD_NOT_FOUND DLL = dll01.dll Proc = funcA1 HMODULE = 0x00000000 delay load error.
dll02.dllをリネーム時:
> ren dll02.dll _dll02.dll > main02.exe funcA1(2, 3) = 6 funcB1(2, 3) = 7 delay load : ERROR_MOD_NOT_FOUND DLL = dll02.dll Proc = funcA2 HMODULE = 0x00000000 delay load error.
dll02.cで、funcA2のdllexport宣言を削除してみる(=funcA2が見つからなくなる):
> main02.exe funcA1(2, 3) = 6 funcB1(2, 3) = 7 delay load : ERROR_PROC_NOT_FOUND DLL = dll02.dll Proc = funcA2 HMODULE = 0x003b0000 delay load error.
Web上で検索した記事のいくつかは、C++の例外処理を使って補足している例もあった。
SDKに添付のdelayhlp.cppを見てみると、例外発生のRaiseException()を呼ぶ前に__pfnDliFailureHook2()を呼んでいることが分かる。
遅延読み込みでは、LoadLibrary()やGetProcAddress()の直前で__pfnDliNotifyHook2()を呼んでいる。
このように、遅延読み込みの処理状況を通知する為の通知ハンドラ、LoadLibrary()/GetProcAddress()失敗で、実際にRaiseException()する前にリカバリ出来るエラーハンドラのフックが提供されている。
delayimp.h中では外部公開シンボルとして定義されており、アプリケーション側でこれらグローバルシンボルに独自のフック関数のアドレスを設定することができる。
PfnDliHook __pfnDliNotifyHook2 PfnDliHook __pfnDliFailureHook2
関数の型 "PfnDliHook" はdelayimp.hで以下のようにtypedefされている:
typedef FARPROC (WINAPI *PfnDliHook)( unsigned dliNotify, PDelayLoadInfo pdli );
この仕組みを上述のmain02.cに組み込み、より詳しい情報を表示出来るようにしたのが以下に示すmain03.cとなる:
#include <windows.h> #include <stdio.h> #include <DelayImp.h> __declspec(dllimport) int funcA1(int a, int b); __declspec(dllimport) int funcB1(int a, int b); __declspec(dllimport) int funcA2(int a, int b); __declspec(dllimport) int funcB2(int a, int b); FARPROC WINAPI MyDliNotifyHook(unsigned dliNotify, PDelayLoadInfo pdli) { printf("MyDliNotifyHook:\n"); switch (dliNotify) { case dliStartProcessing: printf("\tdliStartProcessing...\n"); break; case dliNotePreLoadLibrary: printf("\tdliNotePreLoadLibrary...\n"); break; case dliNotePreGetProcAddress: printf("\tdliNotePreGetProcAddress...\n"); break; case dliNoteEndProcessing: printf("\tdliNoteEndProcessing...\n"); break; } printf("\tDLL(HMODULE) = %s(0x%08X)\n", pdli->szDll, pdli->hmodCur); if (pdli->dlp.fImportByName) { printf("\tProc = %s\n", pdli->dlp.szProcName); } else { printf("\tOrdinal = 0x%08x\n", pdli->dlp.dwOrdinal); } // tell "do nothing" return 0; } FARPROC WINAPI MyDliFailureHook(unsigned dliNotify, PDelayLoadInfo pdli) { printf("MyDliFailureHook:\n"); switch (dliNotify) { case dliFailLoadLib: printf("\tdliFailLoadLib...\n"); break; case dliFailGetProc: printf("\tdliFailGetProc...\n"); break; } printf("\tDLL(HMODULE) = %s(0x%08X)\n", pdli->szDll, pdli->hmodCur); if (pdli->dlp.fImportByName) { printf("\tProc = %s\n", pdli->dlp.szProcName); } else { printf("\tOrdinal = 0x%08x\n", pdli->dlp.dwOrdinal); } // tell "do nothing" return 0; } DWORD MyDelayLoadExFilter(DWORD ec, EXCEPTION_POINTERS *ep) { DelayLoadInfo *dli = NULL; DelayLoadProc *dlp = NULL; if (ec == VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND)) { printf("delay load : ERROR_MOD_NOT_FOUND\n"); } else if (ec == VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND)) { printf("delay load : ERROR_PROC_NOT_FOUND\n"); } else { return EXCEPTION_CONTINUE_SEARCH; } dli = (DelayLoadInfo*)(ep->ExceptionRecord->ExceptionInformation[0]); printf("\tDLL = %s\n", dli->szDll); dlp = &(dli->dlp); if (dlp->fImportByName) { printf("\tProc = %s\n", dlp->szProcName); } else { printf("\tOrdinal = 0x%08x\n", dlp->dwOrdinal); } printf("\tHMODULE = 0x%08x\n", dli->hmodCur); return EXCEPTION_EXECUTE_HANDLER; } int main() { __pfnDliNotifyHook2 = MyDliNotifyHook; __pfnDliFailureHook2 = MyDliFailureHook; __try { printf("funcA1(2, 3) = %d\n", funcA1(2, 3)); printf("funcB1(2, 3) = %d\n", funcB1(2, 3)); printf("funcA2(2, 3) = %d\n", funcA2(2, 3)); printf("funcB2(2, 3) = %d\n", funcB2(2, 3)); } __except(MyDelayLoadExFilter(GetExceptionCode(), GetExceptionInformation())) { printf("delay load error.\n"); } return 0; }
ビルド:
> cl main03.c delayimp.lib dll01.lib dll02.lib \ /link /DELAYLOAD:dll01.dll /DELAYLOAD:dll02.dll
正常時の出力、DLLが見つからない/関数が見つからないなどの異常時の出力は省略する。