home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/memos/VC++/06, DLLの事前バインド(BindImageEx())

C言語系/memos/VC++/06, DLLの事前バインド(BindImageEx())

C言語系 / memos / VC++ / 06, DLLの事前バインド(BindImageEx())
id: 669 所有者: msakamoto-sf    作成日: 2010-06-03 01:20:09
カテゴリ: C言語 Windows 

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=)"オプションを使って事前バインドを試みる。

参考:

対象: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とEXEのセット、およびBindImageEx()を呼ぶ "mybind.c" から成る。

DLL側(dll01.c, dll02.c)

後で"/BASE"の衝突を実験する為、DLLは二つ用意した。

dll01.c:

#include <windows.h>
 
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:

#include <windows.h>
 
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:

#include <stdio.h>
 
__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:

#include <stdio.h>
#include <windows.h>
#include <imagehlp.h>
 
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:

> 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のヘッダー情報からポイントだけ見ていく:

> 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のインポート情報を確認してみる:

> 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になっている。

> 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で確認してみる。
まずインポート情報から:

> 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のヘッダー情報を確認してみる:

> 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"リンカオプションでそれぞれ衝突しない場所に設定する。

> 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のインポート情報を確認する:

> 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"が設定されたことを確認出来る:

> 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
...

バインドしてみる:

> 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で事前バインドを試みる。
事前バインド前:

> 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で確認:

> 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で確認:

> 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)されていることも確認出来た。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-06-09 21:31:04
md5:0ab670389f8b981e0b32885e381cda29
sha1:3f0b80f97f5b7a423ee2242ee9bcebe7aefee91c
コメント
コメントを投稿するにはログインして下さい。