使用头文件从 DLL 创建导入库






4.61/5 (28投票s)
本教程将指导您完成从第三方 DLL 创建 lib 的过程
引言
有两种使用 DLL 的方法:隐式(或静态)和显式(或动态)。前者需要导入库(*.lib 文件)。通常,此 .lib 文件会随 DLL 一起提供,但对于某些第三方 DLL,情况可能并非如此。因此,要隐式使用此类 DLL,我们必须创建其 .lib 文件。
如果您在网上搜索此主题,您会找到 Microsoft 的 KB 页面 http://support.microsoft.com/kb/131313/en-us 和一篇之前的 CodeProject 文章 libfromdll.aspx。Microsoft 的页面包含大量有用信息,但缺少样本源来阐明此过程,而后一篇则在很大程度上偏离主题。本文将提供基本原理和详细教程以及一个示例源代码包。所以我希望我的文章会很有用。
主要原理
说实话,生成 lib 没有什么神奇的。我们绝对需要导出函数的名称或序号。此外,我们可能还需要一个包含导出函数的参数描述、返回值类型、调用约定等信息(所谓的“原型”或“签名”)的头文件,以便正确使用它们。前者信息可以直接从 DLL 本身获取,而后者需要由 DLL 的供应商/作者提供。
在我看来,lib 文件本身并不包含完整的签名信息;只有函数的签名可能会影响其名称修饰,这直接反映在 lib 中。如果导出的函数使用了 extern
"C" 和 __cdecl
,那么签名将不会影响名称修饰,在这种情况下,我认为可以在没有头文件的情况下创建 lib。但是,如果没有头文件来确保 DLL 的安全使用,lib 将毫无用处。所以,谈论在没有头文件的情况下生成 lib 几乎(如果不是完全)没有意义。
我们将使用 Microsoft Visual C++ (MSVC6) 来制作 lib。回想一下,MSVC 会根据 c/CPP 源文件和 .def(模块定义)文件生成 DLL 和 lib。如果您只使用 __cdecl
调用约定,则 .def 文件不是必需的,但要使用 __stdcall
,我发现它不可或缺。所以,大致来说,我们使用以下主要步骤:
- 根据 DLL 的信息,使用
DUMPBIN
工具编写 .def 文件。 - 编写源代码。
- 让 MSVC 构建 DLL 和 lib。这样我们就完成了。
但是等等,我们如何确保生成的 lib 可以用作原始 DLL 的 lib 呢?事实上,上述源代码只是作为原始 DLL 的“平凡包装器”,也就是说,对于原始 DLL 中的每个导出函数,在包装器中都有一个对应的导出函数,它只做一件事,就是跳转到原始函数的入口。因此,平凡包装器与原始 DLL 具有相同的外部行为,因此我们构建的 lib 可以用作原始 DLL 的 lib。
示例 DLL
为了说明我们的过程,我包含了一个示例 DLL,它假装没有 lib(尽管实际上并非如此)。示例 DLL 的源代码包含在 DllSample.zip 中可供下载,但这并不重要,可以忽略。
示例 DLL (DllSample
) 导出两个函数
BOOL __cdecl NumberList(int begin, int end);
void __stdcall LetterList(int begin, int end, BOOL capital);
我们特意使用了不同的调用约定和返回类型,以便以尽可能通用的方式来说明我们的方法。调用 NumberList
时,它会显示一个消息框,列出从 begin 到 end 的数字。调用 LetterList
时,它会显示一个消息框,列出(begin+1)和(end+1)之间的字母;BOOL 参数 capital 控制字母是否为大写。
编写 DEF 文件
MSVC 带有一个 DUMPBIN
工具,用于转储 Windows 二进制文件的内容。现在,如果 DllSample.dll 在当前目录中,键入 "dumpbin /exports dllsample.dll",则导出的函数将显示如下:
ordinal hint RVA name
2 0 000010A0 LetterList
1 00001000 [NONAME]
这表明 LetterList
既按名称也按序号导出,而 NumberList
(此名称未包含在 DLL 二进制文件中,但头文件将包含其信息)仅按序号导出。通过将 DUMPBIN
的输出复制到剪贴板,粘贴到文本文件中,并根据 def 文件的格式进行适当修改,我们可以得到以下 def 文件:
LIBRARY "DllSample"
EXPORTS
NumberList @1 NONAME
LetterList @2
编写源代码
这一主要步骤进一步包含几个步骤。
编写供调用者使用的头文件
在 DLL 调用方包含的头文件应包含导出函数的原型。原始 DLL 的供应商/作者通常会提供一个原始头文件,但它可能以另一种语言编写或在另一种平台上开发,例如 C++ Builder、Delphi 或 VB,因此我们通常需要对其进行编辑,以便在 MSVC 下成功编译。在我们的示例中,头文件 DllSample.h 的核心部分应该是:
extern "C" __declspec(dllimport) BOOL __cdecl NumberList(int begin, int end);
extern "C" __declspec(dllimport) void __stdcall LetterList
(int begin, int end, BOOL capital);
包含 extern "C" 是为了指示编译器使用 C 风格的名称修饰,以允许用 C 编写的程序使用我们的 lib。包含 __declspec(dllimport) 是可选的,但它有助于稍微提高性能。有关详细讨论,请参阅 Matt Pietrek 的文章 http://msdn.microsoft.com/en-us/magazine/cc301805.aspx。
将 DllSample.dll 重命名为 __DllSample.dll
MakeLib
项目的目标文件不是 MakeLib.dll 和 MakeLib.lib,而是 DllSample.dll 和 DllSample.lib。为避免名称冲突,原始 DLL,即在 DllSample
项目中生成的 DllSample.dll,将被重命名为 __DllSample.dll。
编写 MakeLib 的数据部分
DllUtil.zip 包含三个项目:DynCall
、MakeLib
和 UseLib
。DynCall
用于说明 DLL 的动态调用;MakeLib
,如我们所说,用于生成包装器 DLL 和 lib;UseLib
用于演示 MakeLib
中生成的 lib 可以与原始 DLL 无法获得的 lib 相同地使用。在我们的工作区中包含项目 DynCall
是因为 DynCall
和 MakeLib
共享大部分源代码,并且 DynCall
本身也很有用。
DynCall
和 MakeLib
的数据部分非常相似,但它们也有重要的区别。在两个源代码中,都有许多指向导入函数的指针,但在 DynCall
中它们必须声明为全局的,而在 MakeLib
中它们必须定义为静态的,因为它们不需要外部链接。大致来说,MakeLib.cpp 与函数指针相关的核心代码片段如下所示:
#define FUNCDEF_NumberList(NumberList) \
BOOL __cdecl NumberList(int begin, int end)
#define FUNCDEF_LetterList(LetterList) \
void __stdcall LetterList(int begin, int end, BOOL capital)
// if the function pointers are to be used in a DLL wrapper setting
// they should be defined in file scope, i.e. static variables
#define STATDEF_IMPORT_FUNC(funcname) \
typedef FUNCDEF_##funcname(IMP_##funcname); \
static IMP_##funcname *imp_##funcname;
// Define the imported function pointers as static variables,
// since they need not go public. Please include your specific functions
STATDEF_IMPORT_FUNC(NumberList)
STATDEF_IMPORT_FUNC(LetterList)
这会扩展为以下内容:
typedef BOOL __cdecl IMP_NumberList(int begin, int end);
static IMP_NumberList *imp_NumberList;
typedef void __stdcall IMP_LetterList(int begin, int end, BOOL capital);
static IMP_LetterList *imp_LetterList;
然后很容易看出 imp_xxxx's 是指向导入函数的 static
指针,这些指针将在加载包装器 DLL 时被赋值。
此时,读者可能会想,我为什么使用这么多“奇怪”的宏。如果只有少数几个函数,宏并没有真正用处,但商业 DLL 可能会导出数百个函数,在这种情况下,使用设计精良的宏集可以为我们节省大量打字工作。我对此有直接的经验。
编写 MakeLib 的代码部分
MakeLib
代码的大约一半实现了加载原始 __DllSample.dll、获取导出函数地址并将它们存储在函数指针中进行动态调用的功能。此功能代码与 ImportDllSample.cpp(DynCall
的主源文件)相同,应该很容易阅读。
现在我们来看导出包装函数的实现。如前所述,包装器称为平凡,因为每当调用导出包装函数时,它都会简单地跳转到 __DllSample.dll 中相应原始函数的入口点。但是我们如何实现这一点呢?请参阅以下从 MakeLib.cpp 中摘录的代码片段:
// Defines trivial wrapper of the original function. It contains only one instruction,
// that is, jump to the entry of the original function
#define NAKED_WRAPPER(funcname) \
extern "C" __declspec(naked) FUNCDEF_##funcname(funcname) \
{ \
__asm jmp imp_##funcname \
}
NAKED_WRAPPER(NumberList)
NAKED_WRAPPER(LetterList)
这再次扩展为以下内容:
extern "C" __declspec(naked) BOOL __cdecl NumberList(int begin, int end)
{
__asm jmp imp_NumberList
}
extern "C" __declspec(naked)
void __stdcall LetterList(int begin, int end, BOOL capital)
{
__asm jmp imp_LetterList
}
上述代码的关键是 __declspec(naked)
关键字。MSDN 中对其有详细解释。
验证正确性
现在我们启动 UseLib.exe 来测试正确性。现在的调用链是 UseLib
->(新)DllSample.dll ->(原始)DllSample.dll。所以,如果您想检查 DllSample.lib 是否可以像原始 DllSample.dll 的 lib 一样直接使用。您需要将 __DllSample.dll 重命名回 DllSample.dll,然后运行 UseLib.exe。
结论
我们现在可以看到,这样创建的 DllSample.lib 可以完美地用作原始 DllSample.dll 的导入库。总而言之,上述导入库创建过程有两个优点:
- MakeLib.cpp 具有非常通用的骨架,并且宏的使用有助于最大限度地减少输入工作量。
- 包装器 DLL 有时可用于调查或黑客攻击。包装器函数的实现可以轻松修改为“非平凡”的,以便在跳转到原始函数之前执行一些黑客操作。但是,如果您希望在调用原始函数后进行黑客操作,那么裸机实现可能不再合适。