DLL から「正しい」LIB ファイルを作るには

手元に DLL だけがあって、 それとリンクするために LIB ファイルが欲しいことがあります。 例えば C:\Windows\System32\ntdll.dll の中身を見ると:

C:\> dumpbin /exports c:\Windows\System32\ntdll.dll

ordinalhintRVAname
90000093E0A_SHAFinal
10100009050A_SHAInit
11200009520A_SHAUpdate

となっていますから、これを使って sha1 が計算できそうです。 そこで、ntdll.dll とリンクするような LIB ファイルが欲しいわけです。

しかし Bing 先生に訊いてみると、わらわらと不正な方法が出てきます。 というわけでここに正しい方法をメモしておきます。

結論

まず、いろいろ試してみた結果最も楽な方法を述べます。 (以下 Visual Studio 2012 を使うことが前提)

1. akkarin.cpp を用意する

extern "C" {
    void __stdcall A_SHAFinal (int, int){}
    void __stdcall A_SHAInit  (int, int){}
    void __stdcall A_SHAUpdate(int, int, int){}
} // extern "C"

このソースは、LIB を生成するためだけのものです。 必要最低限のことだけ書けばいいです。

2. akkarin.def を用意する

NAME "ntdll.dll"
EXPORTS
    A_SHAFinal
    A_SHAInit
    A_SHAUpdate

NAME "DLL名" のところには、本当にリンクしたい DLL の名前 (今の場合は ntdll.dll) を書きます。書き忘れると大変なことになります。

なお、関数をエクスポートする方法は:

の三通りがありますが、__stdcall の関数の場合、 上手くいくのは DEF ファイルを書くことだけです。それ以外は失敗します。

3. akkarin.lib を作る

C:\> cl /LD akkarin.cpp /link /NODEFAULTLIB /NOENTRY /def:akkarin.def

このコマンドで、akkarin.dll, akkarin.exp, akkarin.lib の三つのファイルができます。 その際:

akkarin.exp : warning LNK4070: .EXP 内の /OUT:ntdll.dll ディレクティブの指定が出 力ファイル名 'akkarin.dll' と異なっています。ディレクティブは無視されます。

というような警告が出ますが、これは意図した動作ですので問題ありません。

4. akkarin.dll を捨てる

ゴミ箱に akkarin.dll と akkarin.exp を捨てます。

残った akkarin.lib が、最終的に欲しかった LIB ファイルです。

5. 使い方の例

C:\> cl /Ox /EHsc myapp.cpp akkarin.lib

他の方法ではなぜうまくいかないのか

ここからは、上記の方法を採らなければならない理由を説明します。

LIB ファイル基礎知識

ordinal, hint

その前に。dumpbin /exports で表示される ordinal, hint とは何でしょうか。 DLL のシンボルテーブルは、二つのものからなっています:

[A] のインデクスが ordinal、[B] のインデクスが hint です。

動的なリンクが行われる際、まずインポートする人が知らされている hint 値を使って、DLL の [B] を引きます。 引いたときに、その関数名が期待していた関数名なら、 そこに記された [A]のインデクスを使って [A] を引き、アドレスを得ます。 もし期待していた関数名と違ったら、[B] を一から検索して関数名を探します。

というわけで、hint 値は間違っていてもいいのです。 新しく関数が追加されれば、[B] は変わってしまいますから、 Windows のバージョンが違えば hint 値は違うものです (多分)。 ですから気にしないで行きましょう。

IMPORT_OBJECT_HEADER

ここからが本題です。LIB ファイルは

IMAGE_ARCHIVE_START
    IMAGE_ARCHIVE_MEMBER_HEADER
        可変長のデータ
    IMAGE_ARCHIVE_MEMBER_HEADER
        可変長のデータ
    IMAGE_ARCHIVE_MEMBER_HEADER
        可変長のデータ
    :
    :

という構造をしています(各構造体は SDK に定義されています)。 LIB ファイルの中でシンボル名を保持しているのは

IMAGE_ARCHIVE_MEMBER_HEADER
    IMPORT_OBJECT_HEADER
        CHAR[] シンボル名 0x00
        CHAR[] DLL 名 0x00

という箇所です。各シンボルにつき一個ずつ IMAGE_ARCHIVE_MEMBER_HEADER と IMPORT_OBJECT_HEADER が付きます。

IMPORT_OBJECT_HEADER の定義を SDK からコピペしてきましょう:

typedef struct IMPORT_OBJECT_HEADER {
    (省略)
    WORD    Type : 2;                   // IMPORT_TYPE
    WORD    NameType : 3;               // IMPORT_NAME_TYPE
    WORD    Reserved : 11;              // Reserved. Must be zero.
} IMPORT_OBJECT_HEADER;

NameType というメンバがあります。ここに入る値は:

typedef enum IMPORT_OBJECT_NAME_TYPE
{
    IMPORT_OBJECT_ORDINAL = 0,          // Import by ordinal
    IMPORT_OBJECT_NAME = 1,             // Import name == public symbol name.
    IMPORT_OBJECT_NAME_NO_PREFIX = 2,   // Import name == public symbol name skipping leading ?, @, or optionally _.
    IMPORT_OBJECT_NAME_UNDECORATE = 3,  // Import name == public symbol name skipping leading ?, @, or optionally _
                                        // and truncating at first @
} IMPORT_OBJECT_NAME_TYPE;

と定義されています。

という三種類の名前比較法があるのです。 __stdcall の関数は、DLL側では接頭辞も接尾辞もありません。 そしてオブジェクトファイルのほうでは、「_関数名@16」のように接尾辞が付きます。

ですから、NameType == IMPORT_OBJECT_NAME_UNDECORATE でなければいけません

間違った方法 1. lib コマンドを使う

DEF ファイルを書けば、ソースファイルがなくても、 次のコマンドで LIB ファイルが得られます:

C:\> lib /def:akkarin.def /out:akkarin.lib

しかしこの方法で作られた LIB ファイルでは、 NameType == IMPORT_OBJECT_NAME_NO_PREFIX となります。

これではどうやっても DLL 側の「関数名」と、オブジェクトファイル側の「_関数名@16」を 照合できません。ですからこの LIB ファイルは正しくありません。

間違った方法 2. __declspec(dllexport) を使う

ソースファイルの関数定義に __declspec(dllexport) を付加すれば、 DEF ファイルがなくても次のコマンドで LIB ファイルが得られます:

C:\> cl /LD akkarin.cpp /Fe:ntdll.dll /link /NODEFAULTLIB /NOENTRY

しかしこの方法で作られた LIB ファイルでは、 NameType == IMPORT_OBJECT_NAME となります。

これも正しくありません。

その他

#pragma comment(linker, "/EXPORTS:symbol") を使う方法は cl コマンドが成功しないので、僕には正しい使い方がわかりません……。

というわけで

結局、正しい LIB ファイルを作るには、 DEFファイルと偽のソースファイルを用意して、 偽の DLL を作り、その副産物としての LIB を流用するしかないのです。多分。

序数という罠の山

dumpbin /exports は序数 (ordinal) も表示してくれます。 ですから DEF ファイルに:

NAME "ntdll.dll"
EXPORTS
    A_SHAFinal   @ 9
    A_SHAInit    @ 10
    A_SHAUpdate  @ 11

と書きたくなってしまいます。しかしこれは罠です。 DEFファイルに序数を書いてはいけません。絶対にダメです。

序数の指定されたものは、序数を使ってリンクするようになってしまいます。 関数名は決して見に行きません。 ですから DLL のバージョンの違いで序数がずれると、 この LIB ファイルを使って作られた EXE は動かなくなります。

実際、上記の ntdll.dll の序数は、64bit のものです。 ツールが 64bit なので、64bit のシステム DLL を見に行ってしまうのです。 この序数は 32bit 版と異なります。ですから この序数を使った DEF を用いて 作った 32bit 版 EXE は、最初から動作しません。

inserted by FC2 system