程序员自我修养笔记:第九章

第九章 Windows下的动态链接
1.相当于Linux下的共享库,Window系统大量采用dll机制,dll更加强调模块化,经常可以看到Windows平台大量的大型软件都通过升级dll的形式自我完善。
2.基地址和RVA
当一个pe文件被装载时,其进程地址空间的起始地址就是基地址,每个文件都有一个优先装载地址,位于文件头中的Image Base。
通常可执行文件的Image Base不会被占用,因为它总是第一个被装载的文件,dll文件可能会因为被其他已装载的dll文件而进行Rebasing。
RVA就是一个相对地址。当PE被装载到0x10000000,RVA为0x1000的地址为0x10001000。

2.dll的数据段在每个进程中都是独立的,但允许被设置成共享的。dll可以一个数据段私有一个数据段共享,这种方式被用来作为一种进程间的通信方式。

3.elf共享库默认导出所有全局符号,而dll中需要显式告诉编译器要导出的符号。
MSVC编译器和支持Windows平台的编译器支持指定符号导入导出的扩展。
__declspec(dllexport)
__declspec(dllimport)
这实际上是通过目标文件的“.drectve”段指示编译器实现的。
在c++中,若希望导入导出的符号符合C的符号修饰规范,必须在符号的定义前加上external “C”
除了扩展关键字,可以使用.def脚本声明导入导出的符号。且对其他语言也有效。
还可以在链接时提供“/EXPORT:_SYMBOL”参数指定导出符号。

4.创建dll
cl /LDd Math.c
MSVC编译器编译debug版的dll
cl /LD Math.c
生成release版
(cl Math.c
生成exe可执行文件)
上面编译结果生成
.dll .obj .exp .lib四个文件

使用dll
cl /c TestMath.c
link TestMath.obj Math.lib

.lib文件(导入库)包含.dll的导出符号以及“桩代码”,以便将程序与dll粘在一起。

5.使用.def文件(模块定义文件)
除了__declspec(dllexport)导出函数,还可以使用.def文件,类似链接脚本,用于控制链接过程,但功能比后者少。
#Math.def
LIBRARY Math
EXPORTS
Add
Sub
Mul
Div
编译Math.c:
cl Math.c /LD /DEF Math.def
.def文件的好处:
可以给导出函数符号起别名,比如当Add函数采用“__stdcall”函数规范,可以在.def文件用
Add=_Add@16,这样Add作为别名会被放到导出函数列表内(与_Add@16的RVA相同)。且.def支持“HEAPSIZE”、“NAME”控制输出文件的默认堆大小、输出文件名等。

6.dll显示运行时链接
Windows提供了3个API:
Loadlibrary、GetProcAddress、FreeLibrary。

7.导出表
PE文件头有一个叫DataDirectory的数组,其第一个元素就是导出表的地址和长度。
导出表结果中最重要的是最后三个表
导出地址表(EAT)、符号名表、名字序号对应表。
符号名表是导出符号表按照ASCII顺序排序的,符号名表和名字序号对应表一一映射,先通过名字找到符号名表对应下标,然后找到对应序号,序号对应着EAT的下标+Base。EAT保存着各个符号的地址。使用序号导入导出方便了名字查找过程,函数名表也不必保存在内存中,是早起内存不够的补救手段,但这种方法受限于序号可能发生变化。手工指定函数导出序号的方法可以通过.def文件实现。

8.EXP文件
是连接器产生的中间目标文件,保存着收集的所有导出符号的导出表(保存在.edata段中)。
9.导出重定向
EXPORTS
HeapAlloc=NTDLL.RtlAllocHeap

10.导入表
类似于elf中的.got和.got.plt,Windows也有类似机制,但名称直接叫导入表。
导入表是一个IMAGE_IMPORT_DESCRIPTOR结构体数组,其中FirstThunk指向一个导入地址数组(IAT),其值表示导入符号的序号或是符号名;动态链接器在完成该模块的链接时,元素值会被动态链接器改写成该符号的真正地址。
那么如何判断每个IAT项是对应一个序号还是符号则通过看这个元素的最高位,对于32位的PE来说,最高位置1那么低31位就是导入符号的序号值;如没有,则值是一个指向IMAGE_IMPORT_BY_NAME结构的RVA。其是由一个WORD和一个字符串组成,WORD是一个“Hint”,其是序号可能的值,动态链接器会根据该“Hint”去定位在目标导出表中的位置,若是需要的符号,命中;若不命中,则按照正常的二分查找进行符号查找。
导出表结构中还有一个OriginalFirstThunk指向一个数组叫做导入名称表(Import Name Table),INT。其跟IAT一模一样。
Windows的动态链接器会在装载模块的时候,改写导入表的IAT,类似于.got。但导入表的一般是只读的,一般位于“.rdata”段中,动态连接器其实是Windows内核的一部分,所以他将导入表所在位置页面改成可读写的,一旦IAT修改完,在将这些页面设回只读。因此PE做法更安全,因为elf运行程序可以随意修改.got,而PE不可以。

11.延迟载入
当你链接一个支持延迟载入的DLL时,当延迟载入的API第一次被调用时,由链接器添加的特殊桩代码就会启动,这个桩代码负责对DLL的装载工作,然后这个桩代码调用GetProcAddress来找到被调用API的地址,另外MSVC还做了优化,使得后续对该DLL的调用速度与普通方式载入的DLL速度相差无几。

因为PE没有类似ELF的共享对象的全局符号介入问题,因此PE对导入函数的引用都是通过直接调用命令,不是间接调用。
CALL XXXXXXXX
对于模块内部还是外部的函数引用问题,MSVC可以使用__declspec(dllimport)来显示声明,这样编译器会在想应的导入库中链接。若不,则在外部函数调用的情况下,将这条call指令导向到一段桩代码JMP DWORD PTR [XXXXXXXX],其中XXXXXXXX为导入符号的IAT地址。而这段桩代码实际上就在导入库内。
编译器在产生导入库时,同一个导出函数会产生两个符号的定义,比如foo就有foo和__imp__foo,其中__imp__foo指向foo函数在IAT中的位置(地址),foo这个符号则指向foo函数的桩代码。对于使用扩展字则链接到__imp前缀的符号,否则链接没有的。
这两种方法都会链接到正确的地址,但是使用扩展字的性能上会少一条跳转指令。

12.影响DLL性能的两个原因。
DLL代码段和数据段本身并不是地址无关的,若ImageBase被占用则需要rebase。大量的rebase会使程序启动速度变慢。
动态链接过程中,导入的符号在运行时需要逐个被解析,即使对符号字符串进行二分查找,这个过程耗时仍然严重。

13.Rebasing
PE的dll中的代码段并不是地址无关的,在装载时若目标地址被占用则会对每个绝对地址进行重定位—加上目标装载地址与实际装载地址的差值。PE文件的重定位信息都放在“.reloc”段中,可从文件头中的DataDirectory里得到重定位段的信息。因为exe文件是第一个装入虚拟地址的,msvc编译器默认不会产生重定位段,但编译器一般都会给dll产生重定位信息,可以在编译时使用“/FIXED”参数禁止dll产生重定位信息,但可能会造成dll装载失败。
这种Rebasing方法会造成每个进程都会拥有dll的单独的代码段副本,且该段被换出时需要写到交换空间中,而不像elf只需释放物理页面因为可以直接从dll文件中重新读取代码段。好处是elf的PIC机制比pe的运行速度慢,因为对于外部数据和函数引用或者数据段访问不需要通过got的机制。空间换时间。
可在dll链接时指定输出文件的基地址。
link /BASE:0x10010000,0x10000 /DLL bar.obj
其中0x10000指定dll占用空间的最大长度,如果超过那么编译器会给出警告。
还可使用editbin改变已有dll的基地址
editbin /REBASE:BASE=0x10020000 bar.dll
Windows自带许多系统dll,基本都是Windows的应用程序运行时要用到的。
专门划出一块0x70000000~0x80000000用于映射,且调整这些dll的基地址防止冲突。

14.序号
一个dll的导出函数可以没有函数名,但是必须有唯一的序号。当我们从dll导入函数时,可以两者取一。序号标示导出函数地址在导出表的位置。
一般,仅供内部使用的导出函数,只有序号,防止外部使用者误用。
可以在.def文件定义导出函数的序号。
LIBRARY Math
EXPORTS
Add @1
div @4 NONAME
其中div表示该符号仅以序号形式导出。

可以使用editbin对exe或dll进行绑定,以便省略不必要的每次的符号解析。
editbin /BIND Testmath.exe
editbin对被绑定的程序的导入符号进行重定位,把符号运行时目标地址写入导入表内,也就是当时提到的多余的与IAT一样的INT数组中。
dll更新导致dll的导出函数地址变化或dll装载时发生Rebasing。
对于第一种情况,PE将导入的dll,把它的dll的时间戳和校验和保存到PE的导入表中,这样运行时会核对装载时和绑定时的dll是否版本相同,不同则会对dll的符号进行解析。
Windows系统所附带的程序都是与他所在的版本的系统dll绑定的。绑定过程会改变PE文件本身。从而导致可执行文件的校验和变化,这对于经过加密的或数字签名的程序来说。可能会有问题。

16.C++程序的兼容性的根源是由于C加加的标准值规定的语言层面上的规则,而对二进制级别的确没有规定。
17.dll hell产生原因有三:
使用旧版本的dll替代新版本的dll
由新版的dll中的函数无意发生改变而引起。
新版dll的安装引入了一个新bug
解决方法:
使用静态链接
防止dll覆盖
避免dll冲突
manifest机制。
在读取可执行文件时,会首先读取程序集的manifest文件,获取其调用的dll列表,再根据dll的manifest文件去寻找对应的dll并调用。

猜你喜欢

转载自blog.csdn.net/weixin_45719581/article/details/123207706