导入库文件(.lib)的结构





5.00/5 (12投票s)
本文展示了与头文件一起用于链接 DLL 的导入库文件(.lib)的结构。
引言
您是否曾经好奇过 Microsoft 的 import
库文件的内容?本文简要介绍了 .lib 文件结构,并提供了根据 export module
(.dll
;.sys
;.exe
文件)的名称和其导出的函数列表(函数可以被挑选,所以不一定需要提供完整列表)来创建导入库的源代码。
关于调用约定和名称修饰的几句话
在 x86 上,我们可以为 C function
使用以下调用约定:
__cdecl
__stdcall
__fastcall
__vectorcall
在 x64 上,我们可以为 C function
使用以下调用约定(编译器会忽略 __cdecl
、__stdcall
、__fastcall
):
- 默认(函数名称不被修饰)
__vectorcall
有关名称修饰的详细信息,请参阅此链接。
导入库结构
导入库是一个归档文件,它以 arch signature
开头,这是一个 8 字节的 字符串
,即:
!<arch>\n
签名之后,我们有一堆文件。
每个文件都以固定长度的头部开始,后面是任意长度的体部(头部包含体部的长度)。
现在我们来考虑 import
库“托管”的 symbols
。假设相关的 export module
是 BOOTVID.DLL,我们将有三个“预定义”的 symbols
:
__IMPORT_DESCRIPTOR_BOOTVID
__NULL_IMPORT_DESCRIPTOR
<7Fh> BOOTVID_NULL_THUNK_DATA
//string
以0x7F
开头
对于每个导入的函数,我们将有两个额外的符号。让我们考虑这个 C function:
void __stdcall VidDisplayString(char *pString);
在 import
库的 x86 版本中,我们将有:
_VidDisplayString@4
// 修饰后的函数名__imp__VidDisplayString@4
// 与string
相同,前缀为__imp_
在 import
库的 x64 版本中,我们将有:
VidDisplayString
__imp_VidDisplayString
// 与string
相同,前缀为__imp_
现在我们来描述每个文件的内容。归档中的前两个文件包含所有符号名称以及指向其他文件的偏移量。然后,我们有三个文件专门用于三个“预定义”符号。最后,我们有专门用于函数的(每个函数一个文件)文件。
文件 1 包含 符号数量 字段、符号名称及其对应的偏移量(相对于归档开头的其他文件偏移量)。因此,文件 3 专用于 __IMPORT_DESCRIPTOR_BOOTVID
,文件 4 专用于 __NULL_IMPORT_DESCRIPTOR
,依此类推。请注意,尽管一个函数会引入两个符号,但每个函数只专用于一个文件。
文件 2 包含偏移量表、符号名称及其对应的索引(指向偏移量表的索引)。它本质上是以不同形式给出的相同信息。请注意,索引从 1
开始。
文件 3、4、5 分别专用于 __IMPORT_DESCRIPTOR_BOOTVID
、__NULL_IMPORT_DESCRIPTOR
、<7F> BOOTVID_NULL_THUNK_DATA
。我不会详细展示它们,因为它们对于所有 import
库来说基本相同。相反,我想重点关注 文件 6
并详细展示它。让我们看看表示文件头部的结构。
struct FILE_HEADER // size = 0x3C
{
char Part1[16];
char Id[24];
char Part3[8];
char BodyLength[10];
char Part5[2];
};
Id
字段包含由 time
函数生成的随机数(以 ASCII 形式),BodyLength
字段包含后续文件体部的长度(以 ASCII 形式)。文件头部之后,是符号描述符。
struct SYMBOL_DESCRIPTOR // size = 0x14
{
WORD a;
WORD b;
WORD c;
WORD Architecture;
DWORD Id;
DWORD Length;
union
{
WORD Hint;
WORD Ordinal;
WORD Value;
}
WORD Type;
};
Architecture
字段包含 x86 的 0x14C
和 x64 的 0x8664
。Id
字段是以二进制形式表示的相同随机数。Hint
/ Ordinal
字段包含函数提示/序号。类型该字段指定 import
类型(稍后会找到描述)。Length
字段包含后面两个 string
(包括它们的 null
字符)的总长度。
导入/导出场景
我们将考虑三种可能的场景:
- 我们编译
export module
并导出函数,而不使用模块定义文件。输入
来源
__declspec(dllexport) void __cdecl function1() {} __declspec(dllexport) void __stdcall function2() {} __declspec(dllexport) void __fastcall function3() {} __declspec(dllexport) void __vectorcall function4() {}
x86 输出
导入库 (
SYMBOL_DESCRIPTOR
)Name = _function1, Hint = 0, Type = 8 Name = _function2@0, Hint = 1, Type = 4 Name = @function3@0, Hint = 2, Type = 4 Name = function4@@0, Hint = 3, Type = 4
导入库存储:提示(从
0
开始,表示AddressOfNames
数组的索引)。Export module
的导出 (AddressOfNames
,AddressOfNameOrdinals
)Name = _function1, Ordinal = index into AddressOfFunctions array Name = _function2@0, Ordinal = index into AddressOfFunctions array Name = @function3@0, Ordinal = index into AddressOfFunctions array Name = function4@@0, Ordinal = index into AddressOfFunctions array
尽管名称为
Ordinal
,但AddressOfNameOrdinals
存储的是Index
(从0
开始,表示AddressOfFunctions
数组的索引)。Import module
的导入 (IMAGE_IMPORT_BY_NAME
)Name = _function1, Hint = 0 Name = _function2@0, Hint = 1 Name = @function3@0, Hint = 2 Name = function4@@0, Hint = 3
x64 输出
导入库 (
SYMBOL_DESCRIPTOR
)Name = function1, Hint = 0, Type = 4 Name = function2, Hint = 1, Type = 4 Name = function3, Hint = 2, Type = 4 Name = function4@@0, Hint = 3, Type = 4
导入库存储:提示(从
0
开始,表示AddressOfNames
数组的索引)。Export module
的导出 (AddressOfNames
,AddressOfNameOrdinals
)Name = function1, Ordinal = index into AddressOfFunctions array Name = function2, Ordinal = index into AddressOfFunctions array Name = function3, Ordinal = index into AddressOfFunctions array Name = function4@@0, Ordinal = index into AddressOfFunctions array
尽管名称为
Ordinal
,但AddressOfNameOrdinals
存储的是Index
(从0
开始,表示AddressOfFunctions
数组的索引)。Import module
的导入 (IMAGE_IMPORT_BY_NAME
)Name = function1, Hint = 0 Name = function2, Hint = 1 Name = function3, Hint = 2 Name = function4@@0, Hint = 3
- 我们编译一个 DLL,并使用模块定义文件导出函数(我们指定函数名)。
输入
来源
void __cdecl function1() {} void __stdcall function2() {} void __fastcall function3() {} void __vectorcall function4() {}
Def 文件
EXPORTS function1 function2 function3 function4
x86 输出
导入库 (
SYMBOL_DESCRIPTOR
)Name = _function1, Hint = 0, Type = 8 Name = _function2@0, Hint = 1, Type = 0xC Name = @function3@0, Hint = 2, Type = 0xC Name = function4@@0, Hint = 3, Type = 0xC
导入库存储:提示(从
0
开始,表示AddressOfNames
数组的索引)。Export module
的导出 (AddressOfNames
,AddressOfNameOrdinals
)// function names are not decorated Name = function1, Ordinal = index into AddressOfFunctions array Name = function2, Ordinal = index into AddressOfFunctions array Name = function3, Ordinal = index into AddressOfFunctions array Name = function4, Ordinal = index into AddressOfFunctions array
尽管名称为
Ordinal
,但AddressOfNameOrdinals
存储的是Index
(从0
开始,表示AddressOfFunctions
数组的索引)。Import module
的导入 (IMAGE_IMPORT_BY_NAME
)// function names are not decorated Name = function1, Hint = 0 Name = function2, Hint = 1 Name = function3, Hint = 2 Name = function4, Hint = 3
x64 输出
导入库 (
SYMBOL_DESCRIPTOR
)Name = function1, Hint = 0, Type = 4 Name = function2, Hint = 1, Type = 4 Name = function3, Hint = 2, Type = 4 Name = function4@@0, Hint = 3, Type = 0xC
导入库存储:提示(从
0
开始,表示AddressOfNames
数组的索引)。Export module
的导出 (AddressOfNames
,AddressOfNameOrdinals
)// function names are not decorated Name = function1, Ordinal = index into AddressOfFunctions array Name = function2, Ordinal = index into AddressOfFunctions array Name = function3, Ordinal = index into AddressOfFunctions array Name = function4, Ordinal = index into AddressOfFunctions array
尽管名称为
Ordinal
,但AddressOfNameOrdinals
存储的是Index
(从0
开始,表示AddressOfFunctions
数组的索引)。Import module
的导入 (IMAGE_IMPORT_BY_NAME
)// function names are not decorated Name = function1, Hint = 0 Name = function2, Hint = 1 Name = function3, Hint = 2 Name = function4, Hint = 3
- 我们编译一个 DLL,并使用模块定义文件导出函数(我们指定函数名和序号)。
输入
来源
void __cdecl function1() {} void __stdcall function2() {} void __fastcall function3() {} void __vectorcall function4() {}
Def 文件
EXPORTS function1 @1 function2 @2 function3 @3 function4 @4
x86 输出
导入库 (
SYMBOL_DESCRIPTOR
)Name = _function1, Ordinal = 1, Type = 0 Name = _function2@0, Ordinal = 2, Type = 0 Name = @function3@0, Ordinal = 3, Type = 0 Name = function4@@0, Ordinal = 4, Type = 0
导入库存储
Ordinal
(从1
开始,表示备用函数名)。Export module
的导出 (AddressOfNames
,AddressOfNameOrdinals
)// function names are not decorated Name = function1, Ordinal = index into AddressOfFunctions array Name = function2, Ordinal = index into AddressOfFunctions array Name = function3, Ordinal = index into AddressOfFunctions array Name = function4, Ordinal = index into AddressOfFunctions array
尽管名称为
Ordinal
,但AddressOfNameOrdinals
存储的是Index
(从0
开始,表示AddressOfFunctions
数组的索引)。如果我们为某个函数使用了NONAME
指令,那么它在AddressOfNames
和AddressOfNameOrdinals
数组中将没有对应的条目。Import module
的导入(现在我们有Ordinal
而不是IMAGE_IMPORT_BY_NAME
)Ordinal = 1 Ordinal = 2 Ordinal = 3 Ordinal = 4
x64 输出
导入库 (
SYMBOL_DESCRIPTOR
)Name = function1, Ordinal = 1, Type = 0 Name = function2, Ordinal = 2, Type = 0 Name = function3, Ordinal = 3, Type = 0 Name = function4@@0, Ordinal = 4, Type = 0
导入库存储
Ordinal
(从1
开始,表示备用函数名)。Export module
的导出 (AddressOfNames
,AddressOfNameOrdinals
)// function names are not decorated Name = function1, Ordinal = index into AddressOfFunctions array Name = function2, Ordinal = index into AddressOfFunctions array Name = function3, Ordinal = index into AddressOfFunctions array Name = function4, Ordinal = index into AddressOfFunctions array
尽管名称为
Ordinal
,但AddressOfNameOrdinals
存储的是Index
(从0
开始,表示AddressOfFunctions
数组的index
)。如果我们为某个函数使用了NONAME
指令,那么它在AddressOfNames
和AddressOfNameOrdinals
数组中将没有对应的条目。Import module
的导入(现在我们有Ordinal
而不是IMAGE_IMPORT_BY_NAME
)Ordinal = 1 Ordinal = 2 Ordinal = 3 Ordinal = 4
按名称导入
当我们按 Name
导入函数时,我们会按以下方式获取其地址:
if (strcmp(FunctionName, AddressOfNames[Hint]))
{
for (int i = 0; i < NumberOfNames; ++i)
{
if (!strcmp(FunctionName, AddressOfNames[i]))
{
Hint = i
break
}
}
}
Index = AddressOfNameOrdinals[Hint]
Address = AddressOfFunctions[Index]
按序号导入
当我们按 Ordinal
导入函数时,我们会按以下方式获取其地址:
Address = AddressOfFunctions[Ordinal - Base]
关于其他 def 文件指令的几句话
INTERNALNAME
允许您在 export module
的代码中使用另一个名称(“internal
”)。请看这个例子:
来源
void __stdcall internal_name() {}
Def 文件
EXPORTS
external_name = internal_name
从导入/导出机制的角度来看,这等同于:
来源
void __stdcall external_name() {}
Def 文件
EXPORTS
external_name
PRIVATE
允许您构建一个不包含某些符号的 import
库(“partial
” import
库)。
使用 dumpbin 获取导出信息
我们可以使用 dumpbin
工具来获取有关 export module
导出信息(我们没有其源代码)的信息。可能的 dumpbin
输出:
这里我们看到修饰后的函数名、提示和序号。
相同,只是函数名未被修饰。
这里只看到序号。由于模块不存储函数名,因此提示也不存在。
请注意,导出始终具有序号,但导出不一定具有名称和提示。
由于调用约定不被存储,我们可以通过反汇编器检查函数代码来确定它。在 x86 上,通常是 __stdcall
或 __fastcall
,在 x64 上,通常是 default
调用约定。
Using the Code
对于每个导入的函数,我们需要指定:
- 名称
- 值
- 参数列表大小(字节)
- 调用约定
- 导入类型
Import
类型:
IMPORT_BY_SPECIFIED_NAME
IMPORT_BY_DECORATED_NAME
IMPORT_BY_ORDINAL
IMPORT_BY_SPECIFIED_NAME
:
Import module
将包含未修饰的函数名和 Hint
,该 Hint
由 Value
参数提供。
Export module
必须存储未修饰的函数名。
IMPORT_BY_DECORATED_NAME
:
Import module
将包含修饰后的函数名和 Hint
,该 Hint
由 Value
参数提供。
Export module
必须存储修饰后的函数名。
IMPORT_BY_ORDINAL
:
Import module
将包含 Ordinal
,该 Ordinal
由 Value
参数提供。
Export module
不需要存储函数名。
Size of argument list in bytes
参数用于执行函数名修饰。
生成导入库的代码
int main(int argc, char* argv[])
{
char *pName;
SYMBOL_INFO *pSymbolList;
pName = "BOOTVID";
pSymbolList = CreateSymbolList(pName);
g_InfoAll.bX64 = FALSE; // specify TRUE for x64
AddFunction(pSymbolList, "VidDisplayString", 4, 4,
CALLING_CONVENTION_STDCALL, IMPORT_BY_SPECIFIED_NAME);
WriteImportLibrary(pName, ".dll", pSymbolList);
DestroySymbolList(pSymbolList);
return 0;
}
这里,我们指定了架构(x86
)、export module
名称(BOOTVID
)、export module
扩展名(.dll)和函数列表。
在我们的 import module
的源代码中,我们将以如下方式声明函数:
__declspec(dllimport) void __stdcall VidDisplayString(char *pString); // C function
后记
我们尚未考虑变量、DATA
和 CONSTANT
指令、使用 .def 文件进行导入、__imp_
的目的以及 C++ 名称。我稍后会更新文章来涵盖所有这些主题。感谢您的阅读。