9.1.1DLL知识要点
DLL即动态链接库,相当于linux下的共享对象,区别在于dll更加强调模块化,由于此特点windows上的程序可通过升级dll的方式进行自我更新。Dll和exe文件实际上是一样的概念,都是具有PE格式的二进制文件。PE就是我们平常安装u盘上,且可以让电脑从优盘启动的最小系统。另外需要注意的是,dll文件不一样后缀都是dll,可能是.ocx表示一种控件,或者是.cpl表示控制面板程序。
Windows下也广泛使用了ELF的动态链接方式,从而实现运行时加载,比如著名的ActiveX技术。ELF可以看做一种数据的存储结构,是一种文件格式。ActiveX是一种控件,可以辅助拓展的实现一些功能,利用IE的Flash播放器就是一个ActiveX控件。
9.1.2DLL共享数据段
Win32下,不同进程运行同一个dll,为了实现进程之间的通信,一个dll中会包含两个数据段,一个进程间共享,另一个私有。由于,多进程共享数据,故任意一个进程的共享数据被恶意破坏,都将导致其他进程出现问题,故从这个角度出发,dll的共享数据段实现进程间通信应该尽量避免。
9.1.3 DLL的简单例子
ELF中,默认导出所有的全局符号,其他模块都可以使用。但是dll中,需要显示告诉编译器需要导出某个符号,否则默认不导出。一些windows平台的编译器,通过__declspec(dllexport)关键字修饰,指定导入导出符号。除了此关键字,在.def拓展名的文件中是类似于ld的链接器的链接脚本文件,可以被当做link链接器的输入文件,用于控制链接过程。.def文件中的IMPORT或者EXPORTS段可以用来声明导入导出符号的段。
9.1.4创建dll
使用MSVC(Microsoft Visual C++)中的cl编译器编译dll命令为:cl /LDd Math.c(/LDd表示debug版本,/LD表示release版本)。通过dumpbin /EXPORTS Math.dll可以查看dll中导出的符号。
9.1.5使用dll
程序使用dll的过程就是引用dll中导出的函数和符号的过程,又叫做导入过程。Dll导出函数或符号,而使用dll导出的函数或符号即为导入过程。在elf中,使用外部模块符号,不需要额外的声明其来自于其他共享对象。但是dll中需要用关键字__declspec(dllimport)来声明。
Math.c在被编译成库会生成三个文件Math.dll、Math.exp、Math.exp,其中.lib文件是一组目标文件的集合,是用来描述.dll的导出符号,便于将程序和dll粘在一起。
9.1.6使用模块定义文件
.def和__declspec(dllexport)拓展都可以声明dll中的某个函数为导出函数。.def的链接过程跟链接脚本作用类似,用于控制链接过程,但是相比于后者,语法更简单,功能也更少。.def中可以控制导出符号名,符号名会根据函数调用规范的不同,而被修饰为不用的名字。关于“__cdecl”、“__stdcall”、“__fastcall”三者的描述如下:
__cdecl,__stdcall是声明的函数调用协议.主要是传参和弹栈方面的不同。一般c++用的是__cdecl,windows里大都用的是__stdcall(API);__cdecl是C/C++和MFC程序默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定。采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。__cdecl可以写成_cdecl。__stdcall调用约定用于调用Win32 API函数。采用__stdcall约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈。__stdcall可以写成_stdcall。__fastcall约定用于对性能要求非常高的场合。__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。__fastcall可以写成_fastcall。
9.1.7dll显示运行时链接
与ELF类似,dll也支持运行链接,即运行时加载。
**9.2符号导出导入表
9.2.1导出表**
例如dll将符号导出给exe文件使用。符号导出表可理解为导出符号与符号地址的映射关系。导出表结构体如下
以Math.dll为例如下
编译器编译出的目标文件.obj中包含.drectve段,其中保存了4个/EXPORT参数(如对于Math.obj里边的四个函数),用于传递给链接器,告知链接器导出相应的函数。
9.2.2导入表
某个程序需要使用来自dll的函数或者变量,这种行为就是符号导入。在ELF中,.rel.dyn和.relplt中分别保存了该模块所需要导入的变量和函数符号以及所在的模块等信息。而**.got和.got.plt保存了这些符号的真正地址**。
对于windows,动态链接器其实是windows内核的一部分,所以他可以随心所欲的修改PE装载以后的任意一部分内容,以及页面属性,故在装载dll时,可以将导入表所在位置的页面从只读修改为可读写,一旦导入了IAT表,便将页面设置为只读属性。从某些角度来看,PE的做法比ELF更加安全,因为ELF运行程序可以随意修改.got,而PE不允许。
9.2.3导入函数的调用
PE dll的代码段并不是地址无关的。未解决PE装载时,模块在进程空间中地址冲突的问题,采用了一种重定基地址的方法。
9.3dll优化
通过对dll分析可知,dll的代码段和数据段本身并不是地址无关的。他们会被装载到一个有ImageBase指定的目标地址中,如果目标地址被占用,那么需要装载到其他地址。当应用程序需要大量的dll时,便会进行大量的符号解析和rebase工作,严重影响速度,故需要采取重定基地址的方式。
PE文件的重定位信息都放在了“.reloc”段。重定位基地址导致如下问题:当dll文件被多个进程调用时,会导致同一代码多次备份,浪费内存。当重定位代码被换出时,为了加快运行速度,会采取将换出代码放置在swap区,从而实现空间换时间的目的。
一个dll中每一个导出函数都有一个对于的序号(ordinal number)。一个导出函数可以没有函数名,但是它必须有唯一一个序号。序号标示被导出函数地址在dll导出表中的位置。一般来说,那些仅供内部使用的导出函数,它只有序号没有函数名,故外部使用者无法推测他的含义和使用方法,以防止误用。
使用序号作为导入方法比函数名方法稍微要快一点点,但在目前硬件条件下,这种提高非常有限,而且目前采用函数名的方法,已将函数名排序,查找时采用二分查找法,所以性能还是很快的。
9.4 C++与动态链接
使用C++编写共享库比C语言复杂的多。当在C++中,增加成员变量或者函数时发布了新的共享库,当使用者采用直接替换的方式,由于程序实例化了原dll,故当访问成员变量时,会出现访问出错的问题。另外,当成员函数要返回类似于字符串时,由于是返回dll中堆上的空间,但是用户程序的堆空间与其不一样,故用户释放此内存的时候必然会释放出错。解决此类问题的方法是新发布的库重命名以区分旧版的库,然后增加库中的释放内存空间函数。
由于C++编写dll时会遇到很多很多类似兼容性的问题,为解决此类问题,微软公司很早就开始了组件对象模型(COM,component object model)的开发工作,它的主要目的之一就是为了解决这些在程序开发中遇到的兼容性问题,推荐阅读《COM本质论》。
9.5dll hell
解决dll hell的方法:
第一点:采取静态链接的方法,这是最简单的方法,从而使得程序运行时不需要依赖dll。
第二点:防止dll覆盖。目前windows中,使用windows文件保护(WFP)技术来缓解,禁止未经授权的dll覆盖。
第三点:避免dll冲突。让每个应用程序拥有自己的一份依赖dll文件。把问题dll的不同版本放到该应用程序文件夹中,而非系统文件夹中。
第四点:.net框架下避免dll hell。采取“强文件名”的方式,即每个执行程序和dll文件对于一个mainfest文件,其中会包括但不限于记录需要的dll文件,以及他们的系统类型、版本号、平台环境等信息,故可以保证系统中存在多个不同版本的库共存而不发生冲突,以及应用程序可以加载正确的dll文件。