WIN32汇编语言教程:第11章 动态链接库和钩子 · 11.1 动态链接库(5)
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章 动态链接库和钩子 · 11.1 动态链接库(4) 下页:第11章 动态链接库和钩子 · 11.1 动态链接库(6)