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)来声明函数。