#navi_header|C言語系| DLLの事前バインドとは、DLLがロードされた時のエクスポートシンボルのアドレスを事前に計算しておき、IATに予め書き込んでおく仕組み。 多数のDLLをロードするEXEでは、DLLのロードと再配置+IATの書き換えで起動時間が遅くなる場合がある。DLLを事前バインドしておくことで、IATの書き換えをスキップでき起動時間の短縮に効果がある。 ただしDLLのバージョンはもとよりOSのバージョン・環境が異なれば当然、ロードされるアドレスも変わる。開発環境で事前バインドしても、実際に動作するクライアント環境ではバインドされたアドレスが無効となる可能性がある。そのため通常はインストール時に、つまりクライアント上で事前バインドを行う。 開発環境で事前バインドを試す場合は、VisualStudio付属ツールの"BIND.exe"か、"EDITBIN.exe"を使う。VSのバージョンによっては"BIND.exe"が無い場合もあるかも知れない。VC++2008ExpressEditionでは"EDITBIN.exe"が提供されている。あるいはクライアント上でのインストール時に事前バインドを行いたい場合は、イメージヘルプAPI(imagehlp.dll)のBindImageEx()APIを使う。 本記事ではBindImageEx()APIを使って事前バインドを試み、その影響をdumpbinなどで確認してみる。 最後にSDK提供の"EDITBIN /BIND(:PATH=)"オプションを使って事前バインドを試みる。 参考: - モジュールのロードを高速化する方法 -- http://www7a.biglobe.ne.jp/~tsuneoka/win32tech/9.html - Need for Binding an Executable to DLLs - CodeProject -- http://www.codeproject.com/KB/DLL/NeedBind.aspx - What is DLL import binding? - The Old New Thing - Site Home - MSDN Blogs -- http://blogs.msdn.com/b/oldnewthing/archive/2010/03/18/9980802.aspx 対象: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 上で行っている。 #more|| #outline|| ---- * サンプルコード DLLとEXEのセット、およびBindImageEx()を呼ぶ "mybind.c" から成る。 ** DLL側(dll01.c, dll02.c) 後で"/BASE"の衝突を実験する為、DLLは二つ用意した。 dll01.c: #code|c|> #include int __declspec(dllexport) func1(int a, int b) { OutputDebugString("func1() start"); return a + b + 1; } int __declspec(dllexport) func2(int a, int b) { OutputDebugString("func2() start"); return a + b + 2; } 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: #code|c|> #include int __declspec(dllexport) func3(int a, int b) { OutputDebugString("func3() start"); return a + b + 3; } int __declspec(dllexport) func4(int a, int b) { OutputDebugString("func4() start"); return a + b + 4; } 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; } ||< ** EXE側(main.c) 暗黙的リンクでdll01, dll02のfunc1 - func4を順に呼ぶだけの実行ファイル。 main.c: #code|c|> #include __declspec(dllimport) int func1(int a, int b); __declspec(dllimport) int func2(int a, int b); __declspec(dllimport) int func3(int a, int b); __declspec(dllimport) int func4(int a, int b); int main() { printf("func1(2, 3) = %d\n", func1(2, 3)); printf("func2(2, 3) = %d\n", func2(2, 3)); printf("func3(2, 3) = %d\n", func3(2, 3)); printf("func4(2, 3) = %d\n", func4(2, 3)); return 0; } ||< ** BindImageEx()のサンプル(mybind.c)とコンパイル BindImageEx()を実行するサンプル, mybind.c: #code|c|> #include #include #include void PrintErrorMsg(DWORD err) { LPTSTR lpMsgBuf; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0, NULL); printf("%s(%ld:0x%08X)\n", lpMsgBuf, err, err); LocalFree(lpMsgBuf); } // BindImageEx()の進行状況に応じて呼ばれるcallback関数。 BOOL StatusRoutine(IMAGEHLP_STATUS_REASON Reason, PSTR ImageName, PSTR DllName, ULONG_PTR Va, ULONG_PTR Parameter) { printf("[%s:%s] ",ImageName, DllName); switch (Reason) { case BindImportModuleFailed: printf("BindImportModuleFailed(%d)", Reason); break; case BindImportProcedureFailed: printf("BindImportProcedureFailed(%d), FuncName=%s", Reason, Parameter); break; case BindImportModule: printf("BindImportModule(%d)", Reason); break; case BindImportProcedure: printf("BindImportProcedure(%d), FuncName=%s", Reason, Parameter); break; case BindForwarder: printf("BindForwarder(%d), FuncName=%s", Reason, Parameter); break; case BindForwarderNOT: printf("BindForwarderNOT(%d), FuncName=%s", Reason, Parameter); break; case BindImageModified: printf("BindImageModified(%d)", Reason); break; case BindImageComplete: printf("BindImageComplete(%d)", Reason); default: printf("Reason = %d", Reason); } printf("\n"); return TRUE; } int main(int argc, char *argv[]) { char szImageName[MAX_PATH]; char szDllPath[MAX_PATH]; char szSymbolPath[MAX_PATH]; BOOL result; if(argc < 2){ printf("usage: %s image_file\n", argv[0]); return -1; } ZeroMemory(szImageName, sizeof(szImageName)); ZeroMemory(szDllPath, sizeof(szDllPath)); ZeroMemory(szSymbolPath, sizeof(szSymbolPath)); strcpy(szImageName, argv[1]); // DLL探索PATH, シンボル探索PATH共にカレントディレクトリを指定。 GetCurrentDirectory(sizeof(szDllPath), szDllPath); GetCurrentDirectory(sizeof(szSymbolPath), szSymbolPath); printf("ImageName = %s\n", szImageName); printf("DllPath = %s\n", szDllPath); printf("szSymbolPath = %s\n", szSymbolPath); result = BindImageEx( 0, // 0でもOK。詳細はBindImageEx()のMSDN参照。 szImageName, szDllPath, szSymbolPath, (PIMAGEHLP_STATUS_ROUTINE)StatusRoutine); if (!result) { PrintErrorMsg(GetLastError()); } return 0; } ||< mybind.cは予めコンパイルしておく。 > cl mybind.c imagehlp.lib * 事前バインド無し まずは事前バインド無しの、いつも通りの手順でコンパイルしてみる。 > cl /c dll01.c > link /dll dll01.obj > cl /c dll02.c > link /dll dll02.obj > cl main.c dll01.lib dll02.lib dumpbinでポイントとなる部分だけ確認する。 dll01.dll, dll02.dll: #pre||> > dumpbin /headers dll01.dll ... FILE HEADER VALUES ... 2102 characteristics Executable 32 bit word machine DLL OPTIONAL HEADER VALUES ... 10000000 image base (10000000 to 1000CFFF) ... > dumpbin /headers dll02.dll ... FILE HEADER VALUES ... 2102 characteristics Executable 32 bit word machine DLL OPTIONAL HEADER VALUES ... 10000000 image base (10000000 to 1000CFFF) ... ||< どちらもベースアドレスが 0x10000000 となっている。 main.exeのヘッダー情報からポイントだけ見ていく: #pre||> > dumpbin /headers main.exe ... FILE HEADER VALUES ... 103 characteristics Relocations stripped Executable 32 bit word machine OPTIONAL HEADER VALUES ... 400000 image base (00400000 to 0040EFFF) ... 10 number of directories 0 [ 0] RVA [size] of Export Directory B7A4 [ 50] RVA [size] of Import Directory ... 0 [ 0] RVA [size] of Bound Import Directory A000 [ 128] RVA [size] of Import Address Table Directory ... ||< DataDirectoryに着目してみると、"Bound Import Directory"は空っぽになっている。 main.exeのインポート情報を確認してみる: #pre||> > dumpbin /imports main.exe ... dll01.dll 40A110 Import Address Table 40B904 Import Name Table 0 time date stamp 0 Index of first forwarder reference 0 func1 1 func2 dll02.dll 40A11C Import Address Table 40B910 Import Name Table 0 time date stamp 0 Index of first forwarder reference 1 func4 0 func3 KERNEL32.dll ... Summary 3000 .data 2000 .rdata 9000 .text ||< これが事前バインドを行うとどう変化するか、あとで見ていく。 デバッガ上でmain.exeを動作させ、dll01.dll, dll02.dllのロードされるアドレスを見ておく。 | dll01.dll | 0x10000000 - 0x1000D000 | | dll02.dll | 0x00380000 - 0x0038D000 | | main.exe | 0x00400000 - 0x0040D000 | 先にdll01.dllが 0x10000000 上にロードされた為、続くdll02.dllが0x00380000以降に再配置されている。 * 事前バインド有り ** 最初は、DLLの方は変更せずに事前バインドを試みる。 DLLの方は変更しない、つまり、dll01.dll/dll02.dll共にベースアドレスはリンカのデフォルト、0x10000000になっている。 #pre||> > mybind.exe main.exe ImageName = main.exe DllPath = C:\(...)\dll02 szSymbolPath = C:\(...)\dll02 [main.exe:dll01.dll] BindImportModule(5) [main.exe:dll01.dll] BindImportProcedure(6), FuncName=func1 [main.exe:dll01.dll] BindImportProcedureFailed(4), FuncName=func1 [main.exe:dll02.dll] BindImportModule(5) [main.exe:dll02.dll] BindImportProcedure(6), FuncName=func4 [main.exe:dll02.dll] BindImportProcedureFailed(4), FuncName=func4 [main.exe:KERNEL32.dll] BindImportModuleFailed(3) [main.exe:(null)] BindImageModified(9) [main.exe:(null)] BindImageComplete(11)Reason = 11 ハンドルが無効です。 (6:0x00000006) ||< BindImageEx()のDllPath引数を現在ディレクトリのみにしている為、KERNEL32.dllの事前バインドはDLLが見つからず失敗している。 dll01.dll, dll02.dllについては BindImportProcedureFailed が発生している。 処理としては BindImageComplete まで、つまりバインドに失敗したDLLもあるものの、処理としては完了したようだ。 気になるのが、続く「ハンドルが無効です。」メッセージで、ロジック上はBindImageEx()APIがFALSEを返した場合、つまり失敗の場合にGetLastError()に対応したメッセージ文字列を表示する。 実験していた当初は、完全に失敗したと勘違いして色々条件を変えてみたりしていたが、実はこの段階で事前バインドされたAPIがEXEに書き込まれていた。 実際にdumpbinで確認してみる。 まずインポート情報から: #pre||> > dumpbin /imports main.exe ... dll01.dll 40A110 Import Address Table 40B904 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference 10001000 0 func1 00000000 1 func2 dll02.dll 40A11C Import Address Table 40B910 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference 10001020 1 func4 00000000 0 func3 KERNEL32.dll ... Header contains the following bound import information: Bound to dll01.dll [ 6] Thu Jan 01 09:00:06 1970 Bound to dll02.dll [ 6] Thu Jan 01 09:00:06 1970 Bound to KERNEL32.dll [ 0] Thu Jan 01 09:00:00 1970 ... ||< dll01.dllとdll02.dllのインポート情報に変化が見られる。"time date stamp"と "Index of first forwarder reference" の全bitが1になり、また、func1, func4 については実際のアドレスを計算したと思われる値が表示されている(dll01.dll, dll02.dllともにベースアドレス(ImageBase)はリンカのデフォルトである0x10000000)。 加えて、 Header contains the following bound import information: としてヘッダー上の事前バインドされたインポート情報らしきものが表示されている。 続いてdumpbinのヘッダー情報を確認してみる: #pre||> > dumpbin /headers main.exe ... 248 [ 44] RVA [size] of Bound Import Directory ... ||< 大きく異なる点が、DataDirectoryの "Bound Import Directory" に値が設定されている点。このDataDirectoryはIMAGE_BOUND_IMPORT_DESCRIPTOR構造体の配列を参照する。IMAGE_BOUND_IMPORT_DESCRIPTOR構造体はバインドされた時のDLLのタイムスタンプとDLL名、IMAGE_BOUND_FORWARDER_REFの数を含む。 実際にデバッガ上でmemory dumpを確認すると、"dumpbin /imports"で得られた Header contains the following bound import information: Bound to dll01.dll [ 6] Thu Jan 01 09:00:06 1970 Bound to dll02.dll [ 6] Thu Jan 01 09:00:06 1970 Bound to KERNEL32.dll [ 0] Thu Jan 01 09:00:00 1970 に相当するデータが設定されていた。 PEファイルをバイナリエディタでダンプし、IATに該当する箇所をチェックしてみると、確かにfunc1とfunc4のアドレスが書き込まれていることを確認出来る。つまり、EXEファイルの段階でIATに実際のアドレスが格納済になっていることを確認出来た。 デバッガで実際に動かしてみると、DLLとEXEのロードアドレスは次のようになっていた。 | dll01.dll | 0x10000000 - 0x1000D000 | | dll02.dll | 0x00380000 - 0x0038D000 | | main.exe | 0x00400000 - 0x0040D000 | dll02.dllの方が再配置され、IATの方もそれに合わせて変更されていた。 ** "/BASE"オプションで空いているメモリ領域をベースアドレスに指定して事前バインドを試みる。 dll01.dll, dll02.dllのベースアドレスを、"/BASE"リンカオプションでそれぞれ衝突しない場所に設定する。 #pre||> > link /dll /BASE:0x600000 dll01.obj > link /dll /BASE:0x700000 dll02.obj > mybind main.exe ImageName = main.exe DllPath = C:\(...)\dll02 szSymbolPath = C:\(...)\dll02 [main.exe:dll01.dll] BindImportModule(5) [main.exe:dll01.dll] BindImportProcedure(6), FuncName=func1 [main.exe:dll01.dll] BindImportProcedureFailed(4), FuncName=func1 [main.exe:dll02.dll] BindImportModule(5) [main.exe:dll02.dll] BindImportProcedure(6), FuncName=func4 [main.exe:dll02.dll] BindImportProcedureFailed(4), FuncName=func4 [main.exe:KERNEL32.dll] BindImportModuleFailed(3) [main.exe:(null)] BindImageModified(9) [main.exe:(null)] BindImageComplete(11)Reason = 11 ハンドルが無効です。 (6:0x00000006) ||< main.exeのインポート情報を確認する: #pre||> > dumpbin /imports main.exe (...) dll01.dll 40A110 Import Address Table 40B904 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference 00601000 0 func1 00000000 1 func2 dll02.dll 40A11C Import Address Table 40B910 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference 00701020 1 func4 00000000 0 func3 (...) ||< とりあえずfunc1, func4について、DLLに指定したベースアドレスに基づくアドレスが設定されているのを確認出来る。 デバッガで実際に動かしてみると、DLLとEXEのロードアドレスは次のようになっていた。 | dll01.dll | 0x00600000 - 0x0060D000 | | dll02.dll | 0x00700000 - 0x0070D000 | | main.exe | 0x00400000 - 0x0040D000 | メモリ上のIATも、dll01.dll/dll02.dll共にベースアドレスに基づくアドレスが設定されていた。 ** "/ALLOWBIND:NO" リンカオプションを付けてビルドしたDLLだとどうなるか "/BASE"オプションに加え、"/ALLOWBIND:NO" リンカオプションを指定してdll01.dll/dll02.dllをビルドする。 > link /dll /ALLOWBIND:NO /BASE:0x600000 dll01.obj > link /dll /ALLOWBIND:NO /BASE:0x700000 dll02.obj dll01.dll, dll02.dllのヘッダー情報を見てみると、OPTIONALヘッダの"DLL characteristics"で初めて"0x800", "Do not bind"が設定されたことを確認出来る: #pre||> > dumpbin /headers dll01.dll ... OPTIONAL HEADER VALUES ... 800 DLL characteristics Do not bind ... > dumpbin /headers dll01.dll ... OPTIONAL HEADER VALUES ... 800 DLL characteristics Do not bind ... ||< バインドしてみる: #pre||> > mybind main.exe ImageName = main.exe DllPath = C:\(...)\dll02 szSymbolPath = C:\(...)\dll02 [main.exe:dll01.dll] BindImportModule(5) [main.exe:dll01.dll] BindImportProcedure(6), FuncName=func1 [main.exe:dll01.dll] BindImportProcedureFailed(4), FuncName=func1 [main.exe:dll02.dll] BindImportModule(5) [main.exe:dll02.dll] BindImportProcedure(6), FuncName=func4 [main.exe:dll02.dll] BindImportProcedureFailed(4), FuncName=func4 [main.exe:KERNEL32.dll] BindImportModuleFailed(3) [main.exe:(null)] BindImageModified(9) [main.exe:(null)] BindImageComplete(11)Reason = 11 ハンドルが無効です。 (6:0x00000006) ||< ・・・バインドできてしまった(;´Д`)。 リンカオプションのマニュアルを確認してみると、DLLに電子署名をしている場合にバインド不可(DLL characteristicsに0x800指定)にするオプションらしい。今回は電子署名をしていないので、問題なくバインドされてしまった可能性がある。 * その他 以下の点については余力が無い為スルー。 - BindImageEx()がFALSEを返し、GetLastError()が6になる:にも関わらず、ファイル上はfunc1, func4について事前バインドできてしまっている件 - func1, func4しか事前バインドできず、それ以外が失敗してしまう件 - main.exeの"Bound Import Directory"で、IMAGE_BOUND_IMPORT_DESCRIPTOR構造体のTimeDateStampが何故か"6"(1970年云々+6秒)になってしまっている件 - DLLに電子署名した上で"/ALLOWBIND:NO"を指定して、バインド不可になるか確認する件 ** ASLR(Address Space Layout Randomization)との兼ね合いについて Vista, Win7以降に搭載されている ASLR(Address Space Layout Randomization) を使う場合、事前バインドによる起動時間短縮効果は無意味となる。ASLRによりランダムな再配置が発動してしまい、従って事前バインドしたアドレスが無効となり、再計算とIATの更新が発生してしまう。 ** "EDITBIN /BIND(:PATH=)"の効能 BindImageEx()は上手くバインド出来なかったので、SDK提供のツールEDITBINで事前バインドを試みる。 事前バインド前: #pre||> > dumpbin /imports main.exe ... dll01.dll 40A110 Import Address Table 40B904 Import Name Table 0 time date stamp 0 Index of first forwarder reference 0 func1 1 func2 dll02.dll 40A11C Import Address Table 40B910 Import Name Table 0 time date stamp 0 Index of first forwarder reference 1 func4 0 func3 ||< バインド実行: > EDITBIN /BIND:PATH=. main.exe dumpbinで確認: #pre||> > dumpbin /imports main.exe ... dll01.dll 40A110 Import Address Table 40B904 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference 00601000 0 func1 00601020 1 func2 dll02.dll 40A11C Import Address Table 40B910 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference 00701020 1 func4 00701000 0 func3 ... Header contains the following bound import information: Bound to dll01.dll [4C067F22] Thu Jun 03 00:56:18 2010 Bound to dll02.dll [4C067F26] Thu Jun 03 00:56:22 2010 Bound to KERNEL32.dll [ 0] Thu Jan 01 09:00:00 1970 ||< ここまでのバインドでは、わざと"/BIND"にPATH指定を行い、dll01.dll, dll02.dllが存在するカレントディレクトリを指定していた。 今度はPATH指定を省略してみる。デフォルトのDLL探索が行われ、KERNEL32.DLLの事前バインドが行われる筈である。 > EDITBIN /BIND main.exe dumpbinで確認: #pre||> > dumpbin /imports main.exe ... dll01.dll 40A110 Import Address Table 40B904 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference 00601000 0 func1 00601020 1 func2 dll02.dll 40A11C Import Address Table 40B910 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference 00701020 1 func4 00701000 0 func3 KERNEL32.dll 40A000 Import Address Table 40B7F4 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference 7C80934A 266 GetTickCount ... 7C81D37B 3FC SetStdHandle Header contains the following bound import information: Bound to dll01.dll [4C067F22] Thu Jun 03 00:56:18 2010 Bound to dll02.dll [4C067F26] Thu Jun 03 00:56:22 2010 Bound to KERNEL32.dll [49C4F49C] Sat Mar 21 23:07:24 2009 Contained forwarders bound to NTDLL.DLL [49900AEF] Mon Feb 09 19:52:31 2009 ||< このようにKERNEL32.dllについても事前バインドされ、また、一部はNTDLL.dllへ転送(forward)されていることも確認出来た。 #navi_footer|C言語系|