程序员的自我修养: Windows下的动态链接

来源:互联网 发布:日本拉面 知乎 编辑:程序博客网 时间:2024/05/21 22:34

一 DLL

Windows下的DLL文件和exe文件实际上是一个概念,他们都是有PE格式的二进制文件,有些不同的是:PE文件头部中有个符号位表示该文件是EXE或是DLL.

每个进程有一份私有的数据副本, 由于在ELF文件中, 代码段是地址无关的, 所以可以在多个进程之间共享一份代码. 但是DLL的代码并不是地址无关的, 所以他只是在某种情况下可以被多个进程间共享.

 

1. DLL共享数据段与进程间通信

在win32下,可以采用DLL共享数据段的方法实现进程间通信.正常情况下,每个DLL的数据段在各个进程中都是独立的,每个进程都拥有自己的副本.但是windows允许将DLL的数据段设置成共享的,即任何进程都可以共享该DLL的同一份数据段.(可以将其分离出来放到另外一个数据段,然后将这个数据段设置成进程间可共享的.也就是说一个DLL中有两个数据段,一个进程间共享,另一个私有.)

但是也产生了一个安全漏洞, 这用利用DLL共享数据段来实现进程间通信的方法应该尽量避免.

 

2. lib文件, dll文件, obj文件, exp文件的联系

当使用MSVC的编译器cl进程编译时:

可见,总计生成了Math.obj, Math.exp, Math.dll, Math.lib四个文件.

当查看Math.lib文件的导出时,

导出分别为: _add _sub _mul, 并且可知Math.lib中的段有:

当查看Math.dll时,

可知, dll文件可以导出的函数有: add, sub, mul, 我们还可以看到他们的相对地址RVA.

Math.dll文件中的段有:

使用cl /c TestMath.c 编译产生TestMath.obj文件.

当链接时,使用命令link TestMath.obj Math.lib产生TestMath.exe文件.

当查看TestMath.obj的.drectve段时,

可见, 没有要求导出的函数(供链接时使用).

整个过程如下:

Math.lib中并不真正包含Math.c的代码和数据,他用来描述Math.dll的导出符号,他包含了TestMath.obj链接Math.dll时所需要的导入符号以及一部分"桩"代码(胶水代码),以便于将程序与DLL粘在一起. Math.lib又叫"导入库".

Math.exp是链接器在创建DLL时的临时存放导出表的文件.

 

3.模块定义文件.def

__stdcall调用规范也是大多数windows下的编程语言所支持的通用调用规范, 那么作为一个能过广泛使用的DLL最好采用__stdcall的函数规范.而MSVC默认采用__cdecl调用规范,否则他就会使用符号修饰,经过修饰的符号不便于维护和使用,于是采用.def文件对导出符号进行重命名就是一个很好的方案. windows中的WINAPI实际上就是__stdcall, 所以DLL也是采用了导出函数重名名的方法.

关于导出符号, math.c文件修改如下:

结果如下:

add sub经过名字修饰,mul不变.

当我们用如下的def文件时:

结果如下:

 

 

4. 导出重定向

EXPROTS

HeapAlloc = NTDLL.RtlAllocHeap

实现机制: 正常情况下,导出表的地址数组中包含的是函数RVA, 但是如果这RVA指向的位置位于导出表中,那么表示这个符号被重定向了.被重定向了的RVA并不代表该函数的地址,而是指向一个ASCII的字符串,这个字符串在导出表中,它是符号重定向后的DLL文件名和符号名.

 

二 导入导出表

1. 导出表

PE头文件有一个叫DataDirectory的结构数组, 这个数组共有16个元素, 每个元素中保存的是一个地址和一个长度. 其中第一个元素就是导出表的结构的地址和长度. 导出表是一个IMAGE_EXPORT_DIRECTORY的结构体. 他被定义在WINNT.h中:

IMAGE_DATA_DIRECTORY结构如下:

映证了"每个元素中保存的是一个地址和一个长度"的说法.那么就可以说明, 其中第一个元素就是导出表的结构的地址和长度.

导出表的结构如下:

对于链接器来说, 它在链接输出DLL时需要知道哪些函数和变量是要被导出的.
因为对于PE文件来说,默认全局函数和变量是不导出的.

指定要导出的符号的一种方法是: 使用MSVC的__declspec(dllexport)拓展,它
实际上是通过目标文件的编译器指示来实现的
(对于math.obj来说, 它在.drectve段保存了四个/EXPORT参数, 用于传递给
链接器,告知链接器导出相应的函数).

 

5. 导入表

当PE文件加载时, windows加载器的其中一个任务就是将所有需要导入的函数地址确定并且将导入表中的元素调到正确的地址, 以实现动态链接.

在PE文件中, 导入表是一个IMAGE_IMPORT_DESCRIPTOR的结构体数组, 每一个这个结构对应一个被导入的DLL.

 

FirstThunk指向一个导入地址数组IAT.

在动态链接器刚完成映射,还没有开始重定位和符号解析的时候, IAT中的元素表示相对应的导入符号的序号或符号名;

当windows的动态链接器在完成该模块的链接时,元素值会被动态链接器改写成符号的真正地址.

 

那么怎么判断导入地址数组的元素是导入符号的序号还是名字?

如果最高位是1, 低31位就是导入符号的序号值;

如果是0, 低31位就指向一个叫IMAGE_IMPORT_BY_NAME的RVA, IMAGE_IMPORT_BY_NAME由一个WORD和一个字符串组成.那个WORD就表示Hint,即导入符号最有可能的序号值, 后面的字符串是一个符号名. 当使用符号名导入时, 动态链接器先使用Hint值去定位该符号在目标导入表中的位置, 如果是就命中; 否则就按照正常的二分查表进行符号查表.

 

IMAGE_IMPORT_DESCRIPTOR的OrginalFistThunk指向一个数组叫做导入名称表INT(Import Name Table).

 

延迟载入(Delayed Load):

当延迟载入的API第一次被调用时, 由链接器添加的特殊的桩代码就会启动, 这个桩代码负责对DLL的装载工作. 然后这个桩代码通过GetProcAddress来找被掉用API的地址.

 

6. 导入函数的调用

在PE模块中, 需要调用一个导入函数时, 就是使用一个间接调用指令:

CALL DWORD PTR [0x0040D11C]

0x0040D11C对应于IAT中的某一项, 也就是0x0040D11C开始取4个字节作为目标地址.

我们可以看到,0x0040D11C是做个目标地址的地址的常数被写入在指令中的. 可以说, PE DLL的代码段并不是地址无关的.所以要使用重定基地址的方法.

 

对于编译器来说, 他无法判断一个函数是本模块内部的, 还是从外部导入的.因为对于普通模块内部函数调用来说, 编译器产生的指令是这样的:

CALL XXXXXXXX

XXXXXXXX是模块内部的函数地址.

MSVC引入 __declspec(dllexport)属性, 一段一个函数有被声明为 __declspec(dllexport), 那么编译器就知道它是外部导入的, 以便产生相对应的指令形式.

 

三 DLL的优化技术

1. 重定基地址(Rebasing)

对DLL文件的基地址进行装载时重定位.

从上面的分析中我们可以注意到DLL文件有一个段叫.reloc.存放的是重定位信息. 我们同样可以从DataDirectory数组里面得到重定位信息.

这样就导致如果一个DLL被多个进程共享, 且该DLL被这些进程装载到不同的位置, 那么每个进程都需要有一份单独的DLL代码段的副本.

 

2. 导入函数绑定

还可以将DLL导出函数的地址保存到模块的导入表中,就可以省去每次启动时符号解析的过程.这叫导入函数绑定.

用一种检验机制来知道是否DLL导出函数地址发生变化, 即链接器把DLL的时间戳和校验和保存到被绑定的PE文件的导入表中. 运行时,核对版本是否相同.

可以看到绑定的信息.

 

四 DLL HELL

windows缺乏一种很有效的DLL版本控制机制.

解决方法:

1. 静态链接

2. 防止DLL覆盖(DLL Stomping)

3. 防止DLL冲突(放到应用程序的文件夹中)

4. .NET下DLL HELL的解决方案

   用一个叫Manifest的清单文件描述程序集.

 

原创粉丝点击