3. 导入库(lib)
动态库(dll)有两种使用方法:
(1)静态链接:库的发行方提供头文件(.h)和导入库文件(.lib)供用户使用,在链接生成用户目标模块(.dll或.exe)时,链接器根据导入库文件(.lib)的指示在目标模块中生成导入表。目标模块在装载时根据导入表的信息解析符号地址。这种在编译、链接和装载期间确定符号地址的机制称为静态链接。
(2)动态链接:用户模块使用Dynamic-Link Library Functions(LoadLibraryEx、GetModuleHandle和GetProcAddress等,可查阅MSDN)在程序运行期间动态获取动态链接库的符号地址。这种在运行期间确定符号地址的机制称为动态链接。
本篇主要研究导入库(lib)的原理,理解链接如何根据导入库信息将依赖符号绑定到导出符号。
3.1 导入库(lib)示例分析
3.1.1 示例1
(1)示例代码
(a)Math1.c
__declspec(dllexport) double __cdecl Add(double a, double b)
{
return a+b;
}
__declspec(dllexport) double __cdecl Sub(double a, double b)
{
return a-b;
}
__declspec(dllexport) double __cdecl Mul(double a, double b)
{
return a*b;
}
__declspec(dllexport) double __cdecl Div(double a, double b)
{
return a/b;
}
(2)编译方法
cl /c /Za /MDd Math1.c
link /DLL /out:Math1.dll Math1.obj
(3)分析
使用dumpbin查看Math1.lib的信息:
dumpbin /all Math1.lib
由于篇幅的关系此处不贴出输出信息,用下图来总结Math1.lib的内容:
_Add与__imp__Add是同一个符号,_Add(或__imp__Add)将被链接到Math1.dll的导出符号Add,导出符号Add的hint可能是0。
Name type为no preffix表示依赖符号去掉前缀“”,例如依赖符号_Add去掉前缀“”就是导出符号Add。
其他相同,此处略过。
3.1.2 示例2
(1)示例代码
(a)Math2.c
double Add(double a, double b)
{
return a+b;
}
double Sub(double a, double b)
{
return a-b;
}
double Mul(double a, double b)
{
return a*b;
}
double Div(double a, double b)
{
return a/b;
}
(b)Math2.def
LIBRARY Math2
EXPORTS
Add @5
Mul @6
Sub @7
Div @8
(2)编译方法
cl /c /Za /MDd Math2.c
link /DLL /DEF:Math2.def /out:Math2.dll Math2.obj
(3)分析
使用dumpbin查看Math2.lib的信息:
dumpbin /all Math2.lib
由于篇幅的关系此处不贴出输出信息,用下图来总结Math2.lib的内容:
_Add与__imp__Add是同一个符号,_Add(或__imp__Add)将被链接到Math2.dll中的导出序号为5的符号。
Name type为ordinal表示依赖符号链接到指定序号的导出符号,例如依赖符号_Add链接到Math2.dll中序号为5的导出符号。
其他相同,此处略过。
3.1.3 示例3
(1)示例代码
(a)Math3.c
double Add(double a, double b)
{
return a+b;
}
double Sub(double a, double b)
{
return a-b;
}
double Mul(double a, double b)
{
return a*b;
}
double Div(double a, double b)
{
return a/b;
}
(b)Math3.def
LIBRARY Math3
EXPORTS
_Add = Add
_Mul = Mul
_Sub = Sub
_Div = Div
(2)编译方法
cl /c /Za /MDd Math3.c
link /DLL /DEF:Math3.def /out:Math3.dll Math3.obj
(3)分析
使用dumpbin查看Math3.lib的信息:
dumpbin /all Math3.lib
由于篇幅的关系此处不贴出输出信息,用下图来总结Math3.lib的内容:
__Add与__imp___Add是同一个符号, _Add(或__imp___Add)将被链接到Math3.dll中的导出符号_Add,导出符号Add的hint可能是0。
Name type为no preffix表示依赖符号去掉前缀“”,例如依赖符号__Add去掉前缀“”就是导出符号_Add。
其他相同,此处略过。
3.1.4 示例4
(1)示例代码
(a)Math4.c
__declspec(dllexport) double __stdcall Add(double a, double b)
{
return a+b;
}
__declspec(dllexport) double __stdcall Sub(double a, double b)
{
return a-b;
}
__declspec(dllexport) double __stdcall Mul(double a, double b)
{
return a*b;
}
__declspec(dllexport) double __stdcall Div(double a, double b)
{
return a/b;
}
(2)编译方法
cl /c /Za /MDd Math4.c
link /DLL /out:Math4.dll Math4.obj
(3)分析
使用dumpbin查看Math4.lib的信息:
dumpbin /all Math4.lib
由于篇幅的关系此处不贴出输出信息,用下图来总结Math4.lib的内容:
_Add@16与__imp__Add@16是同一个符号,_Add@16(或__imp__Add@16)将被链接到Math4.dll中的导出符号_Add@16,导出符号_Add@16的hint可能是0。
Name type为name表示依赖符号名字与导出符号名字相同,例如依赖符号_Add@16就是导出符号_Add@16。
其他相同,此处略过。
3.1.5 示例5
(1)示例代码
(a)Math5.c
double __stdcall Add(double a, double b)
{
return a+b;
}
double __stdcall Sub(double a, double b)
{
return a-b;
}
double __stdcall Mul(double a, double b)
{
return a*b;
}
double __stdcall Div(double a, double b)
{
return a/b;
}
(b)Math5.def
LIBRARY Math5
EXPORTS
Add
Mul
Sub
Div
MyMul=Mul
(2)编译方法
cl /c /Za /MDd Math5.c
link /DLL /DEF:Math5.def /out:Math5.dll Math5.obj
(3)分析
使用dumpbin查看Math5.lib的信息:
dumpbin /all Math5.lib
由于篇幅的关系此处不贴出输出信息,用下图来总结Math5.lib的内容:
_Add@16与__imp__Add@16是同一个符号,_Add@16(或__imp__Add@16)将被链接到Math5.dll中的导出符号Add,导出符号Add的hint可能是0。
Name type为undecorate表示依赖符号名字去掉修饰就是导出符号名字,例如依赖符号_Add@16去掉修饰就是导出符号Add。
其他相同,此处略过。
3.2 导入函数
通过上一节的分析可以发现,每个导出符号在导入库中都对应有2个依赖符号,其中一个依赖符号是另外一个依赖符号加上前缀“_imp”。
通过静态链接的方法使用动态链接库(dll)时,接口的声明通常如下:
__declspec(dllimport) double Add(double a, double b);
如果接口声明中去掉“__declspec(dllimport)”,程序一样可以正常运行:
double Add(double a, double b);
那么“__declspec(dllimport)”的作用究竟是什么?下面用两个例子来分析该问题。
3.2.1 示例1
(1)示例代码
(a)Math.c
__declspec(dllexport) double Add(double a, double b)
{
return a+b;
}
__declspec(dllexport) double Sub(double a, double b)
{
return a-b;
}
__declspec(dllexport) double Mul(double a, double b)
{
return a*b;
}
__declspec(dllexport) double Div(double a, double b)
{
return a/b;
}
(b)TestMath.c
#include <stdio.h>
#if 1
__declspec(dllimport) double Mul(double a, double b);
__declspec(dllimport) double Div(double a, double b);
__declspec(dllimport) double Add(double a, double b);
__declspec(dllimport) double Sub(double a, double b);
#endif
int main(int argc, char **argv)
{
double result = 0.0;
result = Add(3.0, 2.0);
result = Sub(3.0, 2.0);
result = Mul(3.0, 2.0);
result = Div(3.0, 2.0);
printf("result = %f\n", result);
return 0;
}
(2)编译方法
cl /c /Za /MDd Math.c
link /DLL /out:Math.dll Math.obj
cl /c /Za /MDd TestMath.c
link TestMath.obj Math.lib
(3)分析
使用dumpbin工具查看TestMath.obj的依赖符号:
dumpbin /symbols TestMath.obj
可以看到如果导入符号的接口声明使用“__declspec(dllimport)”进行修饰,则依赖符号为:“__imp__Add” 、“__imp__Div” 、“__imp__Mul”和“__imp__Sub”。
使用dumpbin工具查看TestMath.exe的反汇编指令:
dumpbin /disasm TestMath.exe
可以看到,对符号“__imp__Add” 、“__imp__Div” 、“__imp__Mul”和“__imp__Sub”的调用都只使用call指令进行了一次间接跳转。
3.2.2 示例2
(1)示例代码
(a)Math.c
__declspec(dllexport) double Add(double a, double b)
{
return a+b;
}
__declspec(dllexport) double Sub(double a, double b)
{
return a-b;
}
__declspec(dllexport) double Mul(double a, double b)
{
return a*b;
}
__declspec(dllexport) double Div(double a, double b)
{
return a/b;
}
(b)TestMath.c
#include <stdio.h>
#if 0
__declspec(dllimport) double Mul(double a, double b);
__declspec(dllimport) double Div(double a, double b);
__declspec(dllimport) double Add(double a, double b);
__declspec(dllimport) double Sub(double a, double b);
#endif
int main(int argc, char **argv)
{
double result = 0.0;
result = Add(3.0, 2.0);
result = Sub(3.0, 2.0);
result = Mul(3.0, 2.0);
result = Div(3.0, 2.0);
printf("result = %f\n", result);
return 0;
}
(2)编译方法
cl /c /Za /MDd Math.c
link /DLL /out:Math.dll Math.obj
cl /c /Za /MDd TestMath.c
link TestMath.obj Math.lib
(3)分析
使用dumpbin工具查看TestMath.obj的依赖符号:
dumpbin /symbols TestMath.obj
可以看到如果导入符号的接口声明未使用“__declspec(dllimport)”进行修饰,则依赖符号为:“_Add” 、“_Div” 、“_Mul”和“_Sub”。
使用dumpbin工具查看TestMath.exe的反汇编指令:
dumpbin /disasm TestMath.exe
可以看到,对符号“_Add” 、“_Div” 、“_Mul”和“_Sub”的调用首先使用call指令跳转到桩代码处(FF 25开头的指令),然后由桩代码使用jmp指令跳转到目标地址。
3.2.3 导入函数总结
使用下面的图例能够清楚地看到在导入函数接口声明中是否使用“__declspec(dllimport)”指示时编译器生成的依赖符号:
3.3 导入库总结
由上面5个示例可以看出:
(1)dll中每个导出符号在导入库都有2个对应的依赖符号(symbol),其中一个依赖符号是另一个依赖符号加上前缀“_imp”。
(2)Name type表示依赖符号名字经过Name type指定:的方法转换后得到导出符号名字。
总结一下导出库的作用就是:在链接时,链接器将目标文件(.obj)中的依赖符号在导入库中进行查询。如果依赖符号在导入库中有匹配,则根据Name type指定的方法将依赖符号转换为对应的导入符号,如果导出符号是按序号到处(Name type为ordianl)则链接器将导入序号写入导入表,如果不是按序号导出,链接器将导入符号名字写入导入表;如果依赖符号在导入库中没有匹配到,则链接器将会报告“符号未定义”错误。