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

C言語系/memos/VC++/07, DLLのエクスポート転送(forwarding)

C言語系/memos/VC++/07, DLLのエクスポート転送(forwarding)

C言語系 / memos / VC++ / 07, DLLのエクスポート転送(forwarding)
id: 670 所有者: msakamoto-sf    作成日: 2010-06-04 09:01:23
カテゴリ: C言語 Windows 

DLLにはエクスポートするシンボルの実体を別のDLLに転送(forward)する機能がある。例えばWindowsXP(SP3)上のKERNEL32.DLLのエクスポート情報を表示してみると、NTDLL.DLLへ転送されているシンボルがいくつか見つかる。

> dumpbin /exports c:\WINDOWS\System32\kernel32.dll
...
   708  2C3          RtlCaptureContext (forwarded to NTDLL.RtlCaptureContext)
   709  2C4          RtlCaptureStackBackTrace (forwarded to NTDLL.RtlCaptureStackBackTrace)
   710  2C5          RtlFillMemory (forwarded to NTDLL.RtlFillMemory)
   711  2C6          RtlMoveMemory (forwarded to NTDLL.RtlMoveMemory)
   712  2C7          RtlUnwind (forwarded to NTDLL.RtlUnwind)
   713  2C8          RtlZeroMemory (forwarded to NTDLL.RtlZeroMemory)
...

forwardingによるDLLの階層化やインポートライブラリの集約は、ライブラリやソースの構成・依存関係の整理に役立つだろう。

今回はモジュール定義ファイル(.def)を使ったDLLのforwardingを試してみる。

参考:

対象: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, dll01.c のモジュール定義ファイル dll01.def の3本となる。

dll01.c, dll01.def

dll01.cは"funcA1()"の実体を提供する。

dll01.c:

#include <windows.h>
 
int funcA1(int a, int b)
{
    OutputDebugString("funcA1() start");
    return a + b;
}
 
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;
}

dll01.defで"funcA1()"をエクスポートする。また、"funcB1()"という名前でdll02.cの"funcB2()"をエクスポートする。これにより、"funcB1()"を呼ぶ場合はdll02.cの"funcB2()"に転送される。
dll01.def:

LIBRARY dll01
EXPORTS
    funcA1
    funcB1 = dll02.funcB2

dll02.c

dll02.c側では"funcB2()"の実体を実装する。
dll02.c:

#include <windows.h>
 
int __declspec(dllexport) funcB2(int a, int b)
{
    OutputDebugString("funcB2() start");
    return a * b;
}
 
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;
}

dll01.dll, dll02.dll のビルド

最初にdll02.dllをビルドする:

> cl /c dll02.c
> link /dll dll02.obj

次にdll01.dllをビルドする:

>cl /c dll01.c
>link /dll /def:dll01.def dll01.obj
...
dll01.def : error LNK2001: 外部シンボル "funcB2" は未解決です。
dll01.lib : fatal error LNK1120: 外部参照 1 が未解決です。

dll02側のシンボルを解決出来なかったので、dll02.lib(インポートライブラリ)を追加する:

> link /dll /def:dll01.def dll01.obj dll02.lib

これでdll01.dll, dll02.dllがビルド出来た。

実際にdumpbinでエクスポート情報を確認してみる。

まずdll02.dllのエクスポート情報を見てみる:

> dumpbin /exports dll02.dll
...
    00000000 characteristics
    4C0715B5 time date stamp Thu Jun 03 11:38:45 2010
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

          1    0 00001000 funcB2
...

"funcB2"がエクスポートされていることを確認出来る。

続いてdll01.dllのエクスポート情報を見てみる:

> dumpbin /exports dll01.dll
...
    00000000 characteristics
    4C0715D2 time date stamp Thu Jun 03 11:39:14 2010
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00001000 funcA1
          2    1          funcB1 (forwarded to dll02.funcB2)
...

"funcA1()"がエクスポートされていると同時に、"funcB1()"がdll02(.dll)の"funcB2()"に転送されるようになっているのが確認出来る。

EXE側のサンプルコード

暗黙的リンク(implicit-link)と明示的リンク(explicit-link)で2種類のexeを作る。
後ほどデバッガでdll02.dllのロードタイミングを比べてみる。

暗黙的リンク(implicit-link)する main_implicit.c とビルド

main_implicit.c:

#include <stdio.h>
 
__declspec(dllimport) int funcA1(int a, int b);
__declspec(dllimport) int funcB1(int a, int b);
 
int main() {
    printf("funcA1(2, 3) = %d\n", funcA1(2, 3));
    printf("funcB1(2, 3) = %d\n", funcB1(2, 3));
    return 0;
}

コンパイル時は dll01.lib を指定するだけでよい。dll02.libは不要。

> cl main_implicit.c dll01.lib

インポート情報を見てみると"dll01.dll"として"funcA1", "funcB1"がインポートされることが確認出来る:

> dumpbin /imports main_implicit.exe
...
    dll01.dll
                40A110 Import Address Table
                40B8E0 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                    0 funcA1
                    1 funcB1
...

実行:

> main_implicit.exe
funcA1(2, 3) = 5
funcB1(2, 3) = 6

明示的リンク(explicit-link)する main_explicit.c とビルド

main_explicit.c:

#include <windows.h>
#include <stdio.h>
 
typedef int (CALLBACK* DLLFUNC)(int, int);
 
int main() {
    HINSTANCE hDll;
    DLLFUNC funcA1 = NULL;
    DLLFUNC funcB1 = NULL;
 
    hDll = LoadLibrary("dll01");
    if (NULL == hDll) {
        printf("dll load error.\n");
        return 1;
    }
 
    funcA1 = (DLLFUNC)GetProcAddress(hDll, "funcA1");
    if (!funcA1) {
        printf("GetProcAddress error(funcA1).\n");
        FreeLibrary(hDll);
        return 2;
    }
 
    funcB1 = (DLLFUNC)GetProcAddress(hDll, "funcB1");
    if (!funcB1) {
        printf("GetProcAddress error(funcB1).\n");
        FreeLibrary(hDll);
        return 3;
    }
 
    printf("funcA1(2, 3) = %d\n", funcA1(2, 3));
    printf("funcB1(2, 3) = %d\n", funcB1(2, 3));
    FreeLibrary(hDll);
    return 0;
}

ビルドではインポートライブラリ不要:

> cl main_explicit.c

インポート情報を見てみるとKERNEL32だけが登録されていることが確認出来る:

> dumpbin /imports main_explicit.exe
...
Dump of file main_explicit.exe

File Type: EXECUTABLE IMAGE

  Section contains the following imports:

    KERNEL32.dll
                40A000 Import Address Table
                40B7AC Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                  14C FreeLibrary
...

実行:

> main_explicit.exe
funcA1(2, 3) = 5
funcB1(2, 3) = 6

転送先のDLLのロードタイミング

dll01.dll, dll02.dllとも、DllMain()にOutputDebugString()を組み込んでいる。デバッガ上でデバッグログなどを確認すれば、どのタイミングでDllMain()が呼ばれた、つまりロードされたかを確認出来る。
暗黙的リンク、明示的リンクでどのように転送先のDLL、今回ならdll02.dllがロードされるのかを比べてみる。
デバッガにはOllyDbgを使用している。

暗黙的リンクの場合
暗黙的リンクを使ったmain_implicit.exeの場合、EXEがロードされて実行準備が整った段階で、dll01.dll, dll02.dllともにロードされた。
明示的リンクの場合

main_explicit.exeの場合、dll01.dllは

hDll = LoadLibrary("dll01");

の段階でロードされ、DllMain()が実行される。

dll02.dllは

funcB1 = (DLLFUNC)GetProcAddress(hDll, "funcB1");

の段階でロードされ、DllMain()が実行される。恐らく"funcB1"のエクスポートシンボルを解決する時にdll02.dllへの転送情報を見つけ、自動的にdll02.dllをロードしているものと思われる。

なお、当然であるが

funcA1 = (DLLFUNC)GetProcAddress(hDll, "funcA1");

は"dll01.dll"のメモリ空間上のアドレスを返し、

funcB1 = (DLLFUNC)GetProcAddress(hDll, "funcB1");

は"dll02.dll"のメモリ空間上のアドレスを返す。

"dll01.dll"上にdll02.dll, funcB2を呼ぶようなラッパコードが生成され、そのアドレスを返す訳ではない。

おまけ

dll01.cのバリエーションとして、以下のようなdll03.cを用意してみた:

#include <windows.h>
 
int funcA1(int a, int b)
{
    OutputDebugString("funcA1() start");
    return a + b;
}
 
int funcB1(int a, int b)
{
    OutputDebugString("funcB1() start");
    return a * b;
}
 
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;
}

モジュール定義(dll03.def)ファイルは次の通り:

LIBRARY dll03
EXPORTS
    funcA1
    funcB1 = dll02.funcB2

defファイルでは"funcB1"がdll02.funcB2に転送設定されるが、dll03.cの方でもfuncB1の実体がある。
どうなるか?

> cl /c dll03.c
> link /dll /def:dll03.def dll03.obj
dll03.def : error LNK2001: 外部シンボル "funcB2" は未解決です。
dll03.lib : fatal error LNK1120: 外部参照 1 が未解決です。

・・・どうやらモジュール定義ファイルが優先され、dll02が必要になるらしい。

> link /dll /def:dll03.def dll03.obj dll02.lib
>dumpbin /exports dll03.dll
...
     00000000 characteristics
    4C084189 time date stamp Fri Jun 04 08:58:01 2010
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00001000 funcA1
          2    1          funcB1 (forwarded to dll02.funcB2)

ちなみに

int funcB1(int a, int b)

を"dllexport"付で

int __declspec(dllexport) funcB1(int a, int b)

としてみても、リンクの段階でdll02側に転送される。モジュール定義ファイルが優先される(らしい)。

以上、おまけでした。



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