首页 > 代码库 > Dll的链接使用细节
Dll的链接使用细节
关于Dll
Dll,Exe 都是PE格式的二进制文件。Dll相当于Linux操作系统下的so文件
1 基地址(Base Address)和相对地址(RelativeVirtual Address)
基地址(BaseAddress)和相对地址(Relative Virtual Address)是PE文件的概念,当PE文件被装载的时候,进程空间的起始地址就是基地址,这个值是PE文件中的Image Base的值。在exe文件中,Image Base 的值是0x40 0000; 在Dll中,ImageBase的值是0x 1000 0000.
RVL是相对于基地址的偏移量,因为基地址可能被其他PE占用,所以会发生基地址的重定向,因此在PE中用到的地址都是RVL。
2 关于lib
Dll中生成的lib文件不包含代码和数据,它用来描述dll的导出符号,在程序链接的时候用来找到Dll中的变量以及函数,它里面包含程序链接Dll时候所需要的导入符号以及“桩代码”,所以它起到一个黏合,胶水的作用。
3 导出函数以及符号到Dll中的方法
<1> 使用 declspec(dllexport)
<2> 使用def(模块定义)文件
4 导入Dll
在程序中使用dll用两种方法
<1>隐式加载
使用h文件,lib文件,dll文件。这样的话,lib文件起到了链接程序的作用。
<2>显式加载
使用LoadLibrary函数,GetProcAddress函数,FreeLibaray函数来显式加载。使用这种方式的话比较麻烦,它首先需要定义一个函数指针,然后用GetProcAddress函数来根据函数的名字或者序号来返回函数的地址,最后还要释放库。
由于编译器可能对函数的名字进行了修饰,加上了各种前缀和后缀,所以在使用名字进行查找的时候可能会出现找不到的情形。使用函数的序号呢,因为函数的序号可能会因为Dll的升级而产生了变化,这样的话,使用序号来查找的也会出现难以预料的错误。
5 符号导出表
符号导出表提供了一个符号名和符号地址之间的映射关系,可以通过符号名来查找符号地址。
typedef struct_IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; //not use,always 0 DWORD TimeDateStamp; //file generatetime WORD MajorVersion; //notuse,always 0 WORD MinorVersion; //notuse,always 0 DWORD Name; //model’sreal name DWORD Base; //base DWORD NumberOfFunctions; //maximumof order DWORD NumberOfNames; //numberof Names DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image }IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
用dumpbin查看dll的信息,其中有这样一段如下所示:
Section contains the following exports forVCDll.dll
00000000 characteristics
52C0392A time date stamp Sun Dec 29 23:00:58 2013
0.00 version
1 ordinal base
3 number of functions
3 number of names
ordinal hint RVA name
1 0 0001113B ?mul@@YGHHH@Z
2 1 0001111D ?sub@@YAHHH@Z
3 2 000110C3 add
我们可以看到这个描述和IMAGE_EXPORT_DIRECTORY是对应的。
Number of functions :
函数序号的最大值(所以不一定代表函数的个数)。如果你在def文件中指定一个序号为5的函数,那么即使编号为4的函数不存在,这时候Number of functions 依然等于5
Number of Names :
AddressOfFunctions:
它指向EAT(Export Address Table),里面存放各个函数的RVA。
AddressOfNames:
它指向函数名字表,它是按照ASCII编码来排序的。
AddressOfNameOrdinals:
这个是函数名字和序号的对照表。
<1>序号的优点:
早期的计算机内存很小,如果用函数名字表来保存函数时非常奢侈的事情,因为把几百个函数名字加在到内存中会占用不少的内存,为了解决这个问题就采取了序号的方法,每一个序号对应一个函数,直接根据序号来找到对应的函数地址。
<2>序号的缺陷:
Dll在更新的时候,可能会导致函数的序号发生变化,所以如果可能会造成加载函数的错误。
<3>序号的必要性:
我们找到函数时给据序号来找的,所以序号是必须存在的,相反函数名字不一定是需要的,因为我们根据函数名字来找函数的地址的时候,先要找到函数名字对应的序号,然后再根据序号来查找地址,这也是AddressOfNameOrdinals存在的原因了。
<4>根据序号查找函数
函数对应的序号– Base的值得到索引值,根据这个索引值在EAT中查找相对偏移量地址,就得到了函数的地址了。
以上两图对比说明了NumberOfFunctions的值不一定等于NUmberOfNames。同时寻找函数的地址是根据序号来查找的。
6 关于exp文件
链接器在创建Dll的时候与创建静态链接一样,是两个过程,首先是链接器扫描所有的目标文件并且搜集所有的符号信息并创建导出表,为了方便链接器把导出表信息放到一个临时目标文件中的.edata 段中,这个目标文件就是exp文件。这个exp是个标准的PE/COFF目标文件,只不过后缀时exp而不是obj。
第二遍时候,将exp当做普通目标文件与其他obj文件链接在一起并且输出为Dll,此时exp文件中的.edata段就会输出到Dll文件中并且成为导出表。
7 导出重定向
将导出符号重定位到另一个Dll中,如果我们想重定向某个函数,可以使用模块定义文件,
如:
EXPORTS
xxfunc = xx.dll.Oldfunc
8 导入表
如果在程序中使用了来自某个Dll的函数和变量,这种行为就叫做符号导入。当PE文件被加载的时候,windows加载器的一个任务就是将所有需要导入的函数和符号的地址确定,以实现动态链接的过程。
在PE中导入表是一个IMAGE_IMPORT_DESCRIPTOR的结构体数组。数组中每一个成员对应一个Dll。
typedef struct_IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp // inIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND) DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) }IMAGE_IMPORT_DESCRIPTOR;
<1> FirstThunk
其中FirstThunk指向一个IAT(ImportAddress Table),它是导入表中最重要的的结构,IAT的每一个元素对应一个被导入的符号。在动态链接刚完成映射还没有进行重定位以及符号解析的时候,IAT中元素表示的对应的导入的符号的序号或者符号名;在windows的动态链接器完成该模块的链接时候,元素值被动态链接器改写成该符号的 真正地址。
如果IAT的元素的最高位被置为1,那么剩下的31位就表示序号值,如果不是1,那么元素的值就是指向IMAGE_IMPORT_BY_NAME的RVA,
typedef struct_IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; }IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
其中Hint表示导入符号的最有可能的序号值,Name[1]表示的是符号名。当用符号名字去导入时,链接器会根据Hint的值在目标导出表中找出符号的位置,如果没有找到就使用二分查找法去进行符号的查找。
<2> OriginalFirstThunk
OriginalFirstThunk指向一个INT(Import Name Table)这个数组跟IAT一样,里面的数值也一样。
9 延迟载入
在VisualC++6.0版之前,在运行时加载 DLL 的唯一办法是使用 LoadLibrary 和 GetProcAddress 函数;当使用操作系统的可执行文件或 DLL 被加载之后,操作系统才加载 DLL。从 Visual C++ 6.0 开始,与 DLL 静态链接时,链接器提供了一些选项,将 DLL 的加载延迟到程序调用该 DLL 中的函数时才进行。
当链接一个支持延迟加载的dll的时候,链接器会产生与普通Dll导入非常类似的数据,但是操作系统会忽略这些数据,知道Dll中的API第一次被调用的时候,链接器中添加的特殊的桩代码就会启动,这个桩代码负责对Dll的装载工作,它调用GetProcAddress来找到函数的地址。
10 导入函数的调用
导入函数的声明declspec(dllimport)xx func(xx );用来声明函数是外部模块的编译器在产生lib库的时候,对同一个函数来说,产生了两个符号定义,针对函数func来说,一个符号是func,另一个是_imp_func。其中func指向桩代码,_imp_func指向func函数在IAT中的的位置。当通过delspec(dllimport) xx func(xx);的时候,编译器会在编译的时候会在改导入函数前面加上_imp_,以确保跟导入库中的_imp_func函数正确链接,如果没有的话就会产生一个正常的func符号,以便跟导入库中的func符号定义相连接。
当把一个函数声明为declspec(dllimport)xx func(xx)的时候,编译器会知道函数是外部导入的,它就会产生一个 CALL [XXX]的间接跳转指令。
如果不加declspec(dllimport)xx func(xx)的话,编译器就不会区分模块内自己定义还是外部导入的,它统一的产生直接调用指令,但是在连链接的时候会把外部函数导向一段桩代码,
桩代码再把控制权交给IAT中的真正地址。
CALL 0x0040100C
….
0x0040100C
JMP DWORD PTR[XXX]
仅仅从此可以看出如果有了declspec(dllimport)的声明,会减少一条跳转指令,所以函数的执行效率会更高点。
附加:_declspec(import)在C ++导出类中的使用。
在C++导出类中如果不适用_declspec(import)的话那么就会导致类中的静态变量不能解析。所以如果类中有静态变量的话就一定使用_declspec(import)来声明函数。
Dll的链接使用细节