一、API链接库
API即Application Programming Interface,是以一种新的方法代替了DOS中用软中断的方式。
WIN32API中包括了大量的函数、结构和消息等,它不仅为应用程序所调用,也是Windows自身的一部分,Windows自身的运行也调用这些API函数。
Win32的API函数放在Windows的动态链接库(DLL)中,DLL是一种Windows的可执行文件,采用的是和 .exe文件同样的PE格式,在PE格式文件头的导出表中,以字符串形式指出了这个DLL能提供的函数列表。应用程序使用字符串类型的函数名指定要调用的函数。
二、动态链接的概念
动态链接库中的代码在程序链接的时候并不会被插入到可执行文件中,在程序运行的时候才将整个库的代码调入内存,所以称为“动态链接”。
如果有多个程序用到同一个动态链接库,Windows在物理内存中只保留一份库的代码,仅通过分页机制将这份代码映射到不同进程的地址空间中,这样不管有多少程序在使用一个库,库代码实际占用的物理内存永远只有一份。
当然,这时候库使用的数据段还是会被映射到不同的物理内存中,多少个程序在使用动态链接库就会有多少份数据段。
三、API函数调用的过程
书中没有介绍函数的具体调用过程,但是综合书本前后的描述,可以大致得出调用的过程如下:
1. 在程序中声明函数。系统的函数声明以头文件的方式存储在.inc文件中,在编写程序时用include语句将.inc文件中囊括到代码中。在编译时编译器会将include XXX.inc的语句替换为.inc文件中的内容。
2. 找到被调用函数的位置。在程序调用已声明的函数时,程序必须知道调用的API函数存在于哪个DLL中,这些信息存储在可执行文件头的导入表中。导入表的信息是源代码在编译时,由链接器根据导入库中的函数名称,装入的动态链接库名称等信息写入可执行文件头的导入表,导入库就是.lib文件,一个DLL文件对应一个导入库,在编写可执行程序的代码时,为了告诉链接程序使用哪个导入库,使用的语句是:includelib 库文件名 或 includelib <库文件名>。
3. 根据动态链接库中的导出表信息校正被调用函数位置。当PE文件被执行的时候,Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中的函数导出信息对被执行文件的IAT表进行修正。
四、动态链接库的文件形式
动态链接库的缩写为DLL,大部分动态链接库是以扩展名为dll的文件形式存在的,但并不是只有dll扩展名的文件才是动态链接库,系统中的某些exe文件、字体文件(*.fon)、一些驱动程序(*.drv和*.sys)、各种控件(*.ocx)和输入法模块(*.ime)等都是动态链接库。
一个文件是否是动态链接库取决于它的文件结构,动态链接库文件和可执行文件同样使用标准的PE文件格式,仅文件头中的属性位不同而已,所以exe文件的一些特征也存在于动态链接库中,比如,在动态链接库中也可以定义并使用各种资源,可以导入并使用其他动态链接库中的函数等。
实际上,系统中大部分包含公用代码的模块——不管扩展名是什么——都有可能是动态链接库。
五、编写动态链接库文件
(一).入口点
与可执行文件一样,动态链接库需要一个入口点。Windows在库加载、卸载、进程中线程的创建和结束等时候调用入口函数,以便动态链接库可以采取相应的动作。
1. Windows会传给入口函数3个参数:
(1)dwReason参数的值表示本次调用的原因,它可能是下面的四种情况之一:
① 当dwReason的值是DLL_PROCESS_ATTACH的时候,表示动态链接库刚被映射到某个进程的地址空间,程序可以在这里进行一些初始化的工作,并返回TRUE表示初始化成功,返回FALSE表示初始化出错,这样库的装入就会失败。这给了动态链接库一个机会来阻止自己被装入。比如,库程序可以在这里申请并保留一些内存,如果申请失败的话就可以返回FALSE告诉Windows,库无法正常工作。
② 当dwReason的值是DLL_PROCESS_DETACH的时候则相反,表示动态链接库将被卸载,库程序可以在这里进行一些资源的释放工作,如将初始化时申请的内存释放,将打开的文件关闭等。以DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH值进行的调用在库的生命周期中只可能出现一次。
③ 当dwReason的值是DLL_THREAD_ATTACH的时候,表示应用程序创建了一个新的线程。当某个线程正常终止的时候,dwReason的值是DLL_PROCESS_DETACH。如果应用程序不是以多线程方式工作的话,就不会有这两种原因的调用;反之,如果应用程序频繁地创建和结束线程,那么入口函数将不断被调用。
(2)hInstDll是动态链接库的模块实例句柄。当使用这个句柄来装入资源的时候,表示资源是定义在库文件中的。对于动态链接库来说,获取这个句柄的唯一途径就是在入口函数被调用的时候保存这个参数,如果在DLL_PROCESS_ATTACH时不将这个句柄保存下来的话,运行时可能就没有其他方法可以获取了。在这里可能有个疑惑:不是可以通过invoke GetModuleHandle,NULL来获取模块实例句柄吗?是的,但是由于动态链接库是代表应用程序运行的,所以,如果在库中调用这个函数,得到的仍然是“宿主”程序的实例句柄,而不是库程序的实例句柄。
(3) dwReserved参数是系统保留的参数,可以不必理会。
2. 入口函数的结构一般如下面所示:
DllEntry proc hInstDLL,dwReason,dwReserved
mov eax,dwReason
.if eax == DLL_PROCESS_ATTACH
;保存hInstDll
;初始化库需要的各种资源
.if 初始化成功
mov eax,TRUE
.else
mov eax,FALSE
.endif
.elseif eax == DLL_THREAD_ATTACH
;为新的线程分配资源
.elseif eax == DLL_THREAD_DETACH
;为线程释放资源
.elseif eax == DLL_PROCESS_DETACH
;释放库使用的资源
.endif
ret
DllEntry Endp
(二). 导出表
与写普通的可执行文件相比,编写动态链接库文件的流程要多编写一个文件,那就是定义文件 *.def。函数在这个文件中定义可以被其他程序调用的函数,在编译时链接器将根据def文件的内容在动态链接库文件的导出表中加入相应的函数。
如果库文件中的函数没有在def文件中指定,那么这个函数就仅能被库文件本身之中的代码调用,而无法在其他应用程序中使用,这样的函数叫做私有函数。
.def文件的格式是:
EXPORTS _IncCounter
_DecCounter
_Mod
六、编译链接动态链接库文件
Sample库文件例子使用的Makefile文件:
DLL = Sample
ML_FLAG = /c /coff
LINK_FLAG = /subsystem:windows /Dll
$(DLL).dll: $(DLL).obj $(DLL).def
Link $(LINK_FLAG) /Def:$(DLL).def $(DLL).obj
.asm.obj:
ml $(ML_FLAG) $<
.rc.res:
rc $<
clean:
del *.obj
del *.exp
编译的时候,使用Ml.exe编译器的方法并没有什么不同,但是使用Link.exe链接程序的时候,必须使用/Dll和/Def选项,/Dll选项告诉链接器输出文件的格式是动态链接库,/Def:filename.def选项用来指定定义了导出函数名称的def文件名,在这个例子中,库文件中没有包含资源,如果包含资源的话,链接时还可以指定资源文件名,一个完整的链接参数如下所示:
Link /DLL /subsystem:windows /Def:filename.def filename.obj filename.res
七、发布动态链接库
当使用Link.exe链接器完成链接工作后,链接器生成3个文件,它们分别以dll,lib和exp为扩展名,dll文件就是动态链接库,而lib文件是供程序开发用的导入库,exp文件是输出库文件,这是链接时的一个副产品,一般没有什么用途,我们可以直接将它删掉。
如果dll文件是作为最终应用程序的一部分发布的,可以仅发布dll文件;如果是当做组件供其他人做二次开发用的,那么开发者就应该为其他程序员同时提供dll文件和lib文件,并且根据情况提供不同语言使用的头文件,头文件中最好为每个导出函数写一个说明,包括函数的功能、参数的数量、类型和定义等,同时写上版权、版本号等信息,以便其他程序员参考使用。
下面举一个导出函数说明的例子:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Version 1.0
; Date: 2004.05.01
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample.dll导出函数:
;
; 1、invoke _IncCounter
; 增加Dll内部计数器的值(最大增加到10)并返回计数值
; 2、invoke _DecCounter
; 减少Dll内部计数器的值(最小减少到0)并返回计数值
; 3、invoke _Mod,dwNumber1,dwNumber2
; 输入:dwNumber1 和dwNumber2 为两个整数
; 输出:两数的模dwNumber1 % dwNumber2
;********************************************************************
_IncCounter proto
_DecCounter proto
_Mod proto dwNumber1:dword,dwNumber2:dword
上述导出说明文件名称为Sample.inc,这是为Sample.dll文件书写的供其他汇编程序员使用的头文件,位置在Chapter11\Dll\Dll中。
八、动态链接库的使用
(一)常规方法
1. 引用方法
① 用include与includelib语句将动态链接库包含进来
② 直接用invoke语句调用引用
2. 缺点:
① 不管程序要用到哪几个dll文件,如果丢失任何一个dll文件,可执行文件将无法被装入执行。
② 任何一个dll文件因为初始化失败而无法装入时,可执行文件也是无法被装入执行的。
③ 不同版本的dll文件中可能增加了一些函数,也可能废弃了一些函数,有时其他使用这个dll文件的程序可能刚好用到不存在的函数,而这个函数在原来版本的dll文件中本来是存在的,此时可执行文件仍然不能被正常装入执行。
(二)动态装入
1. 动态装入的原理
对于采用常规方法调用动态链接库产生的问题,解决办法是不能将动态链接库的导入信息保存在可执行文件的导入表中,也就是说不要让Windows系统来做动态链接库的装入工作,这些工作由应用程序自己的代码来完成。
有3个函数可以用来完成这样的功能:LoadLibrary(装入动态链接库),FreeLibrary(释放动态链接库)和GetProcAddress(获取导出函数地址)。
2. 动态装入的步骤
① 不再需要包含语句将动态链接库包含进来
② 在.const数据段中需要自己定义装入的库文件名和函数名:
szDll db 'Sample.dll',0 ;装入的动态链接库名称
szIncCounter db '_IncCounter',0 ;装入的函数名
szDecCounter db '_DecCounter',0 ;装入的函数名
szMod db '_Mod',0 ;装入的函数名
③ 第三是在使用库中的函数之前需要使用LoadLibrary将库装入,并使用GetProcAddress函数得到函数的入口地址 。函数按下列顺序在不同目录中查找指定的库文件:当前目录、Windows系统目录和PATH环境变量列出的目录。如果这些目录中存在同名的库文件,那么先搜索到的库文件会被装入。
对于方法一中库文件丢失和库的入口函数返回FALSE告诉Windows初始化失败的情况,LoadLibrary函数均返回NULL,这样程序就可以根据情况决定该怎么做,程序可以显示一个提示信息并退出,也可以不使用这个库文件而继续执行。
④ 如果装载动态链接库成功,接下来就是使用GetProcAddress函数来获取库中函数的地址 。
⑤ 在使用GetProcAddress函数获取了库中导出函数的入口点以后,程序在调用的时候一般使用将参数手工入栈的方法,如对_Mod函数的调用可以写为:
push num2
push num1
call lpMod ;lpMod保存有GetProcAddress获取的地址
这样写法的缺点是无法使用invoke伪指令来进行参数检验,容易引发错误。实际上还有一个变通的方法,可以将一个变量定义为子程序入口指针,并为它定义参数个数,方法是两次使用typedef伪操作:
_PROCVAR2 typedef proto :dword,:dword
PROCVAR2 typedef ptr _PROCVAR2
如上面的第一句将_PROCVAR2类型定义为使用两个参数的函数类型,第二句将PROCVAR2类型定义为_PROCVAR2类型的指针,这样,在数据段中就可以将保存函数入口地址的变量使用PROCVAR2类型来定义了,得到的好处就是可以用invoke语句来调用这个变量中的指针:
.data?
lpMod PROCVAR2 ?
最后有一个最重要的概念一定要牢记:动态链接库是被映射到其他应用程序的地址空间中执行的,它和应用程序可以看成是“一体”的,动态链接库可以使用应用程序的资源,它所拥有的资源也可以被应用程序使用,它的任何操作都是代表应用程序进行的,当动态链接库进行打开文件、分配内存和创建窗口等操作后,这些文件、内存和窗口都是为应用程序所拥有的。
注释:
[1] 程序员将实现各种功能的代码写成一个个子程序(函数),编译成obj文件后,用Lib.exe工具将多个obj文件组合成一个lib文件。