Win32汇编动态链接库
1 动态链接库的概念
在DOS环境下编过程序的读者一定知道静态库的含义——程序员将实现各种功能的代码写成一个个子程序(函数),编译成obj文件后,将多个obj文件组合成一个lib文件,当程序中要用到这些函数的时候,只需要指定函数名称,编译器就可以从库中抽出对应的子程序代码插入到可执行文件中去,这样就可以不必一遍遍地重写相同的功能代码。这种链接方法就是静态链接,
Windows的解决办法就是使用动态链接库,动态链接库从表面上看也是提供了一大堆通用的函数,也可以被多个程序使用,但它和静态库的使用上有很多的不同点。
静态库仅在编译的时候使用,编译完成后,可执行文件就可以脱离库文件单独使用了,而动态链接库中的代码在程序编译的时候并不会被插入到可执行文件中,在程序运行的时候才将整个库的代码调入内存,所以称为“动态链接”。如果有多个程序用到同一个动态链接库,Windows在物理内存中只保留一份库的代码,仅通过分页机制将这份代码映射到不同进程的地址空间中,这样不管有多少程序在使用一个库,库代码实际占用的物理内存永远只有一份。当然,这时候库使用的数据段还是会被映射到不同的物理内存中,多少个程序在使用动态链接库就会有多少份数据段。
当应用程序装载动态链接库的时候,程序中仅包括库的名称和函数的名称,这些信息是动态寻找对应函数所必须的,程序在编译和链接的时候必须插入这些定位信息,定位信息取自导入库文件,这一点在前面的编程中已经多次涉及。
动态链接库的缩写为DLL,大部分动态链接库是以扩展名为dll的文件形式存在的,但并不是只有dll扩展名的文件才是动态链接库,系统中的某些exe文件、字体文件(*.fon)、一些驱动程序(*.drv)、各种控件(*.ocx)和输入法模块(*.ime)等都是动态链接库。实际上,系统中大部分包含公用代码的模块——不管扩展名是什么——都有可能是动态链接库。
一个文件是否是动态链接库取决于它的文件结构,动态链接库文件和可执行文件同样使用标准的PE文件格式,仅文件头中的属性位不同而已,所以exe文件的一些特征也存在于动态链接库中,比如在动态链接库中也可以定义并使用各种资源,可以导入并使用其他动态链接库中的函数等。
有一个最重要的概念一定要牢记:动态链接库是被映射到其他应用程序的地址空间中执行的,它和应用程序可以看成是“一体”的,动态链接库可以使用应用程序的资源,它所拥有的资源也可以被应用程序使用,它的任何操作都是代表应用程序进行的,当动态链接库进行打开文件、分配内存和创建窗口等操作后,这些文件、内存和窗口都是为应用程序所拥有的。
2 编写动态链接库
写动态链接库程序应该算是很简单的,程序中并不需要用到新的函数,只是在程序的结构上和链接时的选项有些区别而已。让我们通过一个简单的例子来说明,例子代码在所附光盘的Chapter11\Dll\Dll目录中,包括汇编源文件Counter.asm和定义文件Counter.def。上一层子目录Chapter11\Dll中存放的是调用DLL的例子,其中的内容将在下一节中分析。
Counter.asm的内容如下:
.386
.model flat, stdcall
option casemap :none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
.data?
dwCount dd ?
.code
DllEntry proc _hInstance,_dwReason,_dwReserved
mov eax,TRUE
ret
DllEntry Endp
_SetText proc _hWnd,_dwID,_dwCount
invoke SetDlgItemInt,_hWnd,_dwID,_dwCount,TRUE
ret
_SetText endp
_IncCount proc _hWnd,_dwID
inc dwCount
invoke _SetText,_hWnd,_dwID,dwCount
mov eax,dwCount
ret
_IncCount endp
_DecCount proc _hWnd,_dwID
dec dwCount
invoke _SetText,_hWnd,_dwID,dwCount
mov eax,dwCount
ret
_DecCount endp
End DllEntry
程序很简单,既没有创建窗口的代码也没有创建对话框的代码,总之,程序没有任何和创建界面有关的代码,仅定义了4个子程序:DllEntry,_SetText,_IncCount和_DecCount。其中程序的入口点由最后一句End语句定义到了DllEntry处。(end定义哪个涵数为入口涵数) 而_SetText子程序被最后两个子程序调用,功能是将指定的数值显示到指定对话框的一个子窗口控件中。最后两个子程序的功能分别是对dwCount变量进行递增或递减的操作,并把操作结果显示在参数指定的对话框中的子窗口控件中。
看上去,程序比较莫名其妙——入口的代码什么都没有做,仅返回一个TRUE;也没有地方用到_IncCount和_DecCount函数,这是为什么呢?请记住,dll文件的设计不是供自己使用的,而是被映射到其他应用程序的地址空间中代表“宿主”程序执行的,这两个函数就是供其他程序使用的函数,实际上对于“宿主”程序来说,虽然这两个函数仅包含3行代码,但它们的级别和User32.dll中的CreateWindowEx与DefWindowProc等极其复杂的函数没有任何区别。
3. 入口点和初始化代码
与可执行文件一样,动态链接库需要一个入口点,动态链接库的入口点是一个函数,函数的名称并不重要,例子代码中的入口函数命名为“DllEntry”,读者也可以把它取名为其他任何合法的名字,但入口函数的格式是有规定的。
库的入口函数对调用动态链接库的应用程序来说是不可见的,它仅供操作系统使用。Windows在库装载、卸载、进程中线程的创建和结束等时候调用入口函数,以便动态链接库可以采取相应的动作。在入口函数中可以通过参数来判别Windows的本次调用究竟是在哪种情况下发生的。入口函数的结构一般如下面所示:
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
Windows会传给入口函数3个参数,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。如果应用程序不是以多线程方式工作的话,就不会有这两种原因的调用;反之,如果应用程序频繁地创建和结束线程,那么入口函数将不断被调用。
hInstDll是动态链接库的模块实例句柄。当使用这个句柄来装入资源的时候,表示资源是定义在库文件中的,对于动态链接库来说,获取这个句柄的惟一途径就是保留入口函数的这个参数,如果在DLL_PROCESS_ATTACH时不将这个句柄保存下来的话,运行时可能就没有其他方法可以获取了。dwReserved参数是系统保留的参数,可以不必理会。
读者可能会问:不是可以通过GetModuleHandle(NULL)函数来获取模块实例句柄吗?是的,但是动态链接库是代表应用程序运行的,所以,如果在库中调用这个函数,得到的仍然是“宿主”程序的实例句柄,而不是库程序的实例句柄。
在例子程序中,不需要初始化工作,所以仅返回一个TRUE,表示任何情况下,Windows都可以装入这个库文件。动态链接库有一种很“极端”的应用:纯资源库,这些库仅包含资源而没有任何的功能函数,如字体文件等,对于这些库来说,库中的全部代码仅是入口函数中用来返回TRUE的那几句,这是库能被正常装入所必须的代码。
4. 导出函数
与写普通的可执行文件相比,动态链接库的设计流程中多了一个文件,那就是定义文件 *.def,源代码目录中还包括一个Counter.def文件,它的内容是:
EXPORTS _IncCount
_DecCount
文件内容总共只有两行:一个EXPORTS关键字加上两个库中函数的名称,这是用来告诉链接器这两个函数需要导出,也就是说这两个函数可以被其他程序调用。动态链接库的文件格式是PE格式,每个PE格式文件的文件头中都可以有一个导出表,只有导出表中列出的函数才可以被其他程序调用,链接器根据def文件的内容在导出表中加入由EXPORTS关键字指定的函数名。
如果库文件中的函数没有在def文件中指定(如例子中的_SetText函数),那么这个函数就仅能被库文件中的代码调用,而无法在其他应用程序中使用,这是因为库文件的导出表中没有列出它的名称,这样其他程序根本不会知道它的存在。对于这些函数,可以把它们叫做私有函数。
5. 链接选项
为了生成动态链接库文件,在链接的时候必须使用合适的选项,来看看Counter库文件例子使用的Makefile文件:
DLL = Counter
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
del *.lib
编译的时候,使用Ml.exe编译器的方法并没有什么不同,但是使用Link.exe链接程序的时候,必须使用/Dll和/Def选项,/Dll选项告诉链接器输出文件的格式是动态链接库,/Def:filename.def选项用来指定定义了导出函数名称的def文件名,在这个例子中,库文件中没有包含资源,如果包含资源的话,链接时还可以指定资源文件名,一个完整的链接参数如下所示:
Link /DLL /subsystem:windows /Def:filename.def filename.obj filename.res
6. 生成库文件
当使用Link.exe链接器完成链接工作后,链接器生成3个文件,它们分别以dll,lib和exp为扩展名。dll文件就是动态链接库,而lib文件是供程序开发用的导入库。
回想一下:当在汇编源程序中用到某个动态链接库中的函数时,在源文件的一开始就要用includelib语句指定动态链接库的导入库,这样链接的时候链接器才知道到哪个库中寻找指定的函数,如果开发的时候没有动态链接库的导入库文件,使用起来就比较麻烦了。
为了在开发其他程序的时候使用自己编写的动态链接库,就必须提供这个动态链接库的导入库文件,Link.exe考虑了这一点,所以在生成dll文件的同时也生成了导入库文件。如果dll文件是当做最终应用程序发布的,可以仅发布dll文件;如果是当做插件供其他人做二次开发用的,那么就要为其他程序员同时提供dll文件和lib文件,并且根据情况提供不同语言使用的头文件(最后,还要为每个导出函数写一个说明,包括参数的个数、类型和定义等)。目录中还有一个Counter.inc文件,它的内容如下:
_IncCount proto :dword,:dword
_DecCount proto :dword,:dword
这是为了在汇编程序中使用Counter.dll库文件而书写的include文件。(这样读者就知道MASM32软件包中包含的lib文件和inc文件是怎么来的了!)
exp文件是输出库文件,这是链接时的一个副产品,一般没有什么用途,我们可以直接将它删掉。
7. 使用动态链接库
虽然在前面的学习中一直在使用动态链接库,但本节仍然要介绍一下使用动态链接库的方法,这是为了比较全面地介绍使用动态链接库的不同途径和它们之间的区别。相关的例子文件包含在所附光盘的Chapter11\Dll目录中。在开始分析例子之前,首先要把上一节中生成的相关文件拷贝到本目录中以便使用,它们是Counter.dll,Counter.lib和Counter.inc文件。
例子包括两个不同的程序UseDll1和UseDll2,分别用来演示使用DLL的两种方法。两个例子文件使用了同样的资源定义文件UseDll.rc,所以它们的界面是一样的,事实上,它们完成的功能也是一样的,仅实现的方法不同而已,UseDll.rc文件的内容如下:
#include
#define ICO_MAIN 1000
#define DLG_MAIN 1000
#define IDC_COUNT 1001
#define IDC_INC 1002
#define IDC_DEC 1003
ICO_MAIN ICON "Main.ico"
DLG_MAIN DIALOG 186, 132, 173, 44
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "DLL例子"
FONT 9, "宋体"
{
LTEXT "当前计数", -1, 6, 9, 37, 8
EDITTEXT IDC_COUNT, 45, 7, 69, 12, ES_READONLY
PUSHBUTTON "增加(&A)", IDC_INC, 121, 6, 46, 14
PUSHBUTTON "减少(&D)", IDC_DEC, 121, 23, 46, 14
}
资源中定义了一个如图11.1所示的对话框,上面设计了两个按钮,分别用来调用Counter.dll中的_IncCount和_DecCount函数,这两个函数将DLL内部计数器中的计数进行增减计算,并将结果显示在对话框上的编辑框中。
8. DLL使用例子的运行界面
1. 方法一:常规方法
先看UseDll1.asm程序,这个程序用常规的方法实现了对Counter.dll动态链接库中函数的调用:
.386
.model flat, stdcall
option casemap :none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include Counter.inc
includelib Counter.lib
ICO_MAIN equ 1000
DLG_MAIN equ 1000
IDC_COUNTER equ 1001
IDC_INC equ 1002
IDC_DEC equ 1003
.code
_ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam
mov eax,wMsg
.if eax == WM_CLOSE
invoke EndDialog,hWnd,NULL
.elseif eax == WM_COMMAND
mov eax,wParam
.if ax == IDC_INC
invoke _IncCount,hWnd,IDC_COUNTER
.elseif ax == IDC_DEC
invoke _DecCount,hWnd,IDC_COUNTER
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_ProcDlgMain endp
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam,eax,DLG_MAIN,NULL,\
offset _ProcDlgMain,NULL
invoke ExitProcess,NULL
end start
这段代码是再简单不过的了,读者只需要注意两点。首先是程序开头的包含语句:
include Counter.inc
includelib Counter.lib
第二点就是WM_COMMAND消息中调用_IncCount和_DecCount函数的方法,毋需多言,方法和使用Windows提供的DLL时是一模一样的。
那么,这种方法有什么缺点呢?先来做几个实验。首先,将Counter.dll文件删除,再执行UseDll1.exe文件,这时对话框并没有被显示,系统弹出如图11.2所示的错误提示框。
8.2. 丢失Dll文件时的错误信息
这说明不管程序要用到哪几个dll文件,如果丢失任何一个dll文件的话(UseDll1.exe文件中还用到了Kernel32.dll文件和User32.dll文件,它们并没有丢失),可执行文件将无法被装入执行。
第二个实验:修改上一节中的Counter.asm,将入口函数中的返回值改为FALSE,也就是说模拟dll初始化失败的情况。修改完毕后重新编译链接生成新的Counter.dll文件,并将文件拷贝到UseDll1.exe所在目录后运行,系统将弹出如图8.3所示的错误提示。也就是说,任何一个dll文件因为初始化失败而无法装入时,可执行文件也是无法被装入执行的。
8.3 Dll初始化失败时的错误信息
第三个实验:模拟软件升级或dll文件版本不对时的情况,这种情况在Windows系统中经常发生,因为当某些应用软件包被安装的时候,它可能会用自己附带的某个版本的dll文件替换掉Windows目录中已存在的dll文件,当程序卸载的时候,它有可能会根据备份恢复原来的版本,但更多的情况是根本没有恢复,经过多次安装和卸载不同的应用软件包后,最终的结果就是Windows系统目录中各个dll文件的版本参差不齐。不同版本的dll文件中可能增加了一些函数,也可能废弃了一些函数,有时其他使用这个dll文件的程序可能刚好用到不存在的函数,而这个函数在原来版本的dll文件中本来是存在的。
现在就修改程序来模拟这种情况,将Counter.def文件中的_DecCount一行去掉,这样dll文件的导出表中就不会有这个函数,相当于函数不存在了,然后重新编译dll文件并将它拷贝到UseDll1.exe所在目录,执行UseDll1.exe,这时系统显示的是如图11.4所示的错误提示,UseDll1.exe程序仍然不能被正常装入执行。
8.4 找不到导出函数的错误信息
现在读者一定明白这个最熟悉不过的错误信息的由来了,通常对付这种莫名其妙的错误的最好方法就是重新安装Windows,这将使所有dll文件的版本被重新安装为统一的版本,错误也就自然消失了。读者也可能会说:把出错的dll替换掉不就行了吗,为什么要整个重装呢?问题是你知道原来的版本应该是多少吗?
如果使用这种标准的方法调用动态链接库中的函数,链接器会根据导入库中的信息将使用的dll文件名和函数名存放在可执行文件头的导入表中,这样Windows要执行文件的时候,在装入的过程中会根据导入表中的dll列表寻找每个dll文件,并根据函数名在每个dll中寻找导出函数,如果这中间出现任何错误,如上面演示的dll文件丢失、dll文件装入失败或dll中的函数名无法找到等情况,应用程序都无法被装载执行。
2. 方法二:动态装入
方法一的优点就是使用方便,应用程序可以像使用自己内部的函数一样使用DLL中的函数,缺点也显而易见,如果装入DLL的过程中有任何错误,应用程序没有任何机会完成应变的措施,因为它根本没有被装入执行。
编程中有时候会有下面的需求:
程序需要使用系统中的保留函数。这个函数确实存在于动态链接库的导出表中,可以被其他程序引用,但是软件开发包提供的lib文件中并不包含它。
不同版本Windows中的函数集不同(如Windows NT中的很多与安全有关的函数在Windows 9x中不存在),同一版本Windows中不同版本dll文件的函数集也可能不同,程序需要根据函数是否存在做不同的处理。
程序使用的某些库并不重要(如仅用来显示程序版本的库),如果丢失这个库,程序希望能继续运行,而不是像上面演示的那样出现根本无法装入的情况。
对于这些需求,解决的办法就是不能将动态链接库的导入信息保存在可执行文件的导入表中,也就是说不要让Windows系统来做动态链接库的装入工作,这些工作由应用程序自己的代码来完成。有3个函数可以用来完成这样的功能:LoadLibrary(装入动态链接库),FreeLibrary(释放动态链接库)和GetProcAddress(获取导出函数地址)。
例子UseDll2.asm程序使用这种动态装入的方法来实现UseDll1程序同样的功能,来看看UseDll2.asm的内容:
.386
.model flat, stdcall
option casemap :none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
ICO_MAIN equ 1000
DLG_MAIN equ 1000
IDC_COUNTER equ 1001
IDC_INC equ 1002
IDC_DEC equ 1003
_PROCVAR2 typedef proto :dword,:dword
PROCVAR2 typedef ptr _PROCVAR2
.data?
hDllInstance dd ?
lpIncCount PROCVAR2 ?
lpDecCount PROCVAR2 ?
.const
szError db 'Counter.dll 文件丢失或装载失败,计数功能无法实现',0
szDll db 'Counter.dll',0
szIncCount db '_IncCount',0
szDecCount db '_DecCount',0
.code
_ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam
mov eax,wMsg
;********************************************************************
.if eax == WM_CLOSE
.if hDllInstance
invoke FreeLibrary,hDllInstance
.endif
invoke EndDialog,hWnd,NULL
;********************************************************************
.elseif eax == WM_INITDIALOG
invoke LoadLibrary,addr szDll
.if eax
mov hDllInstance,eax
invoke GetProcAddress,hDllInstance,\
addr szIncCount
mov lpIncCount,eax
invoke GetProcAddress,hDllInstance,\
addr szDecCount
mov lpDecCount,eax
.else
invoke MessageBox,hWnd,addr szError,NULL,\
MB_OK or MB_ICONWARNING
invoke GetDlgItem,hWnd,IDC_INC
invoke EnableWindow,eax,FALSE
invoke GetDlgItem,hWnd,IDC_DEC
invoke EnableWindow,eax,FALSE
.endif
;********************************************************************
.elseif eax == WM_COMMAND
mov eax,wParam
.if ax == IDC_INC
.if lpIncCount
invoke lpIncCount,hWnd,IDC_COUNTER
.endif
.elseif ax == IDC_DEC
.if lpDecCount
invoke lpDecCount,hWnd,IDC_COUNTER
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_ProcDlgMain endp
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam,eax,DLG_MAIN,NULL,\
offset _ProcDlgMain,NULL
invoke ExitProcess,NULL
end start
下面分析这个程序和UseDll1程序的不同点。
首先,程序的开始不再需要Counter.lib文件和Counter.inc文件,少了对应的include和includelib语句,因为并不需要链接器去定位函数的位置。
第二是 .const数据段中需要自己定义装入的库文件名和函数名:
szDll db 'Counter.dll',0 ;装入的动态链接库名称
szIncCount db '_IncCount',0 ;装入的函数名
szDecCount db '_DecCount',0 ;装入的函数名
这些信息原来是由链接器根据lib文件的信息写在可执行文件头的导入表中的,既然现在由程序自己来装入库函数,那么这些信息也就需要自己定义了。
第三是在使用库中的函数之前需要使用LoadLibrary将库装入,并使用GetProcAddress函数得到函数的入口地址。程序中将这个步骤安排在对话框初始化消息WM_INITDIALOG中完成,读者也可以在使用函数前的任何地方完成。LoadLibrary函数的使用方法是:
invoke LoadLibrary,lpDllFileName
.if eax
mov hDllInstance,eax
.endif
参数lpDllFileName指向需要装载的库文件名,库文件名是一个以NULL结尾的字符串。函数按下列顺序在不同目录中查找指定的库文件:当前目录、Windows系统目录和PATH环境变量列出的目录。如果这些目录中存在同名的库文件,那么先搜索到的库文件会被装入。
如果装载成功,函数返回库文件的实例句柄,装载失败则返回NULL。返回的实例句柄需要被保存起来,以后在获取库中的导出函数,装载库中的资源以及释放库的操作中都要用到它。
对于方法一中库文件丢失和库的入口函数返回FALSE告诉Windows初始化失败的情况,LoadLibrary函数均返回NULL,这样程序就可以根据情况决定该怎么做,程序可以显示一个提示信息并退出,也可以不使用这个库文件而继续执行。UseDll2程序的处理方法是显示一个“Counter.dll 文件丢失或装载失败,计数功能无法实现”提示信息,然后将对话框中的“增加”、“减少”两个按钮灰化后继续执行。这样,对话框可以正常显示出来,但是使用库函数的计数功能被屏蔽掉了。
如果装载动态链接库成功,下一步就是使用GetProcAddress函数来获取库中函数的地址。GetProcAddress函数的使用方法是:
invoke GetProcAddress,hDllInstance,lpProcName
.if eax
mov lpProc,eax
.endif
hDllInstance参数就是LoadLibrary函数返回的动态链接库的实例句柄,lpProcName指向要获取的函数名称,函数名也是用以NULL结尾的字符串来定义。有些系统DLL中的函数名称并不是字符串,而是使用数值编号,对于这种情况,lpProcName参数可以指定为函数的编号数值。
如果执行成功,返回值是要获取的函数的入口地址,程序可以保存它并在以后调用。如果执行失败,比如因为版本变化等原因导致需要获取的函数不存在,这时函数返回NULL。
在不再需要动态链接库的时候,为了释放库所占用的系统资源,需要使用FreeLibrary函数释放它。FreeLibrary函数的用法是:
invoke FreeLibrary,hDllInstance
输入参数是LoadLibrary函数返回的实例句柄,函数导致系统以DLL_PROCESS_DETACH代码调用库的入口函数,这样库文件可以自己释放占用的一些资源,然后,整个库的代码和数据被从应用程序的地址空间中清除。
但是在一个应用程序中使用FreeLibrary函数并不会影响另一个应用程序使用同一个库文件,当库文件还被另一个程序使用中的时候,它还是在物理内存中存在。实际上,操作系统为每个库文件维护一个装入计数器,每次使用LoadLibrary装载库文件(或者使用方法一由Windows来装入一个库)的时候,计数器递增;每次使用FreeLibrary函数将库释放的时候,计数器递减,只有到计数器减到零,也就是库文件没有被任何程序使用的时候,操作系统才会将它从物理内存中真正释放掉,否则仅是从某个进程的地址空间中解除了内存映射关系而已。
方法二和方法一的最后一个不同点是调用函数的方法,在使用GetProcAddress函数获取了库中导出函数的入口点以后,程序在调用的时候一般使用将参数手工入栈的方法,如对_IncCount函数的调用可以写为:
push IDC_COUNTER
push hWnd
call lpIncCount ;lpIncCount保存有GetProcAddress获取的地址
这样写法的缺点是无法使用invoke伪指令来进行参数检验,容易引发错误。实际上还有一个变通的方法,可以将一个变量定义为子程序入口指针,并为它定义参数个数,方法是两次使用typedef伪操作:
_PROCVAR2 typedef proto :dword,:dword
PROCVAR2 typedef ptr _PROCVAR2
如上面的第一句将_PROCVAR2类型定义为使用两个参数的函数类型,第二句将PROCVAR类型定义为_PROCVAR2类型的指针,这样,在数据段中就可以将保存函数入口地址的变量使用PROCVAR2类型来定义了,得到的好处就是可以用invoke语句来调用这个变量中的指针:
.data?
lpIncCount PROCVAR2 ?
lpDecCount PROCVAR2 ?
有人曾询问笔者这样一个问题:如果既没有导入库,也没有资料,该如何使用DLL中的函数?答案是,函数名是没有问题的,通过一些工具查看导出表就可以得知库中所有的导出函数列表,但是有关调用函数使用参数的数量和参数的定义方法等资料就成问题了,惟一的办法就是通过反汇编或者跟踪来找出参数的数量和含义后再通过本节介绍的方法调用。
使用方法二时要注意:不管是使用LoadLibrary函数还是GetProcAddress函数,对返回值必须要检查,否则一旦失败的话,很容易引发调用NULL指针的错误。
11.1.4 动态链接库中的数据共享
当多个应用程序同时使用同一个动态链接库的时候,这些动态链接库在系统中是存在于不同进程的地址空间中的,它们代表“宿主”程序工作,互相之间没有任何联系。这一点可以通过一个简单的实验来演示:当我们多次运行UseDll1.exe(或UseDll2.exe),按动不同对话框中的“增加”或“减少”按钮的时候,每个对话框中的计数值按照自己的规律增减,不会受到其他对话框中计数值的影响。这就是说,虽然Counter.dll被多个进程同时装入,但是操作系统为它们映射了各不相同的数据段,使它们工作起来互不影响。
但是需要在进程间进行数据共享的时候,这种互相隔离的特征就不是我们所需要的了,当然,解决的方法之一就是使用第10章中介绍的内存映射文件,但是更简单的办法是通过构造特殊的动态链接库来实现。
再次以前面的Counter.dll为例,现在将它的计数器改成是全局的,也就是说运行UseDll1.exe的多个拷贝的时候,不同对话框增减的是同一个计数器。
回顾第2章对Link.exe程序的介绍,会发现链接器有一个/SECTION选项,可以将某个节区的属性自行定义,选项中有一个S属性,代表将节区的属性设置为共享,这就是我们需要的,实际上不必修改Counter.asm源程序,只需要把Makefile文件中的Link选项修改一下:
DLL = Counter
ML_FLAG = /c /coff
LINK_FLAG = /subsystem:windows /Dll /section:.bss,S
$(DLL).dll: $(DLL).obj $(DLL).def
Link $(LINK_FLAG) /Def:$(DLL).def $(DLL).obj
.asm.obj:
ml $(ML_FLAG) $<
.rc.res:
rc $<
未初始化数据段 .data?的节区名称为 .bbs,加上/section:.bss,S选项就可以将这个段的属性改为共享,这样,当DLL被不同应用程序装载的时候,不但映射到不同进程地址空间中的代码段来自同一段物理内存,.data?段的映射也来自同一段物理内存。
修改Makefile以后来验证一下,使用nmake /a将DLL重新编译并将Counter.dll文件拷贝到UseDll1程序所在目录,然后多次执行UseDll1程序以产生多个对话框,当在一个对话框中按下“增加”按钮将计数增加到x的时候,再换到另一个对话框中再按“增加”按钮,会发现出现的值是x+1而不是原来应该出现的1,表示这些对话框操作的是一个共享的计数器。
如果不希望全部的数据都共享,如hInstance等私有的数据,可以把这些数据放在初始化数据段 .data中,它的节区名称不同于 .data?段,在将 .data?段的属性修改为共享的时候并不会影响 .data段的属性