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

C言語系/memos/VC++/08, DLLの遅延読み込み(delay loading)

C言語系/memos/VC++/08, DLLの遅延読み込み(delay loading)

C言語系 / memos / VC++ / 08, DLLの遅延読み込み(delay loading)
id: 671 所有者: msakamoto-sf    作成日: 2010-06-04 13:16:40
カテゴリ: C言語 Windows 

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, dll02.c)とビルド

遅延読み込みさせる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

EXE側のサンプルコード(main01.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

dumpbinでインポート情報を確認

まずヘッダー情報を見てみると、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まで更新されるわけではない。

DLLが見つからない場合

dll01.dllをリネームして実行してみると、funcA1()の呼び出しで次の例外が発生する。

アドレス:7C812AFB
メッセージ = 例外発生: C06D007E

デバッガ無しで実行すると、「問題が発生したため、main01.exeの実行を~」という一般的な例外発生時のメッセージボックスが表示され、異常終了する。

遅延読み込みの例外発生に対応した main02.c

遅延読み込みで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++の例外処理を使って補足している例もあった。

通知, エラーフックに対応した main03.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が見つからない/関数が見つからないなどの異常時の出力は省略する。

その他

  • 遅延読み込みしたDLLを明示的にアンロードしたい
    • → "/DELAY:UNLOAD"リンカオプションと、delayhlp.cppの __FUnloadDelayLoadedDLL2() を使う。
  • 遅延読み込みされたDLLを全てロードしたい
    • → delayhlp.cppの __HrLoadAllImportsForDll() を使う。
      • 遅延読み込みに伴う例外処理を一箇所でまとめて行いたい時に便利だろう。


プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-06-04 13:27:03
md5:4cb6c1b1f3b8fefe8c4d46a17800a33e
sha1:908cf783eef0c052ae01a6d04bbce649ccf0a206
コメント
コメントを投稿するにはログインして下さい。