当程序中相同功能的一段代码用得比较频繁时,可以将它分离出来写成一个子程序,在主程序中用call指令来调用它。这样可以不用重复写相同的代码,而用call指令就可以完成多次同样的工作了。Win32汇编中的子程序也采用堆栈来传递参数,这样就可以用invoke伪指令来进行调用和语法检查工作。 3.4.1 子程序的定义 子程序的定义方式如下所示。 子程序名 proc [距离][语言类型][可视区域][USES 寄存器列表][,参数:类型]...[VARARG] local 局部变量列表
指令
子程序名 endp proc和endp伪指令定义了子程序开始和结束的位置, proc后面跟的参数是子程序的属性和输入参数。子程序的属性有: ● 距离——可以是NEAR,FAR,NEAR16,NEAR32,FAR16或FAR32,Win32中只有一个平坦的段,无所谓距离,所以对距离的定义往往忽略。 ● 语言类型——表示参数的使用方式和堆栈平衡的方式,可以是StdCall,C,SysCall,BASIC、FORTRAN和PASCAL,如果忽略,则使用程序头部 .model定义的值。 ● 可视区域——可以是PRIVATE,PUBLIC和EXPORT。PRIVATE表示子程序只对本模块可见;PUBLIC表示对所有的模块可见(在最后编译链接完成的 .exe文件中);EXPORT表示是导出的函数,当编写DLL的时候要将某个函数导出的时候可以这样使用。默认的设置是PUBLIC。 ● USES寄存器列表——表示由编译器在子程序指令开始前自动安排push这些寄存器的指令,并且在ret前自动安排pop指令,用于保存执行环境,但笔者认为不如自己在开头和结尾用pushad和popad指令一次保存和恢复所有寄存器来得方便。 ● 参数和类型——参数指参数的名称,在定义参数名的时候不能跟全局变量和子程序中的局部变量重名。对于类型,由于Win32中的参数类型只有32位(dword)一种类型,所以可以省略。在参数定义的最后还可以跟VARARG,表示在已确定的参数后还可以跟多个数量不确定的参数,在Win32汇编中惟一使用VARARG的API就是wsprintf,类似于C语言中的printf,其参数的个数取决于要显示的字符串中指定的变量个数。 完成了定义之后,可以用invoke伪指令来调用子程序,当invoke伪指令位于子程序代码之前的时候,处理到invoke语句的时候编译器还没有扫描到子程序定义信息的记录,所以会有以下错误信息: error A2006: undefined symbol : _ProcWinMain 这并不是说子程序的编写有错误,而是invoke伪指令无法得知子程序的定义情况,所以无法进行参数的检测。在这种情况下,为了让invoke指令能正常使用,必须在程序的头部用proto伪操作定义子程序的信息,“提前”告诉invoke语句关于子程序的信息,proto的用法见3.2.2节。当然,如果子程序定义在前的话,用proto的定义就可以省略了。 由于程序的调试过程中可能常常对一些子程序的参数个数进行调整,为了使它们保持一致,就需要同时修改proc语句和proto语句。在写源程序的时候有意识地把子程序的位置提到invoke语句的前面,省略掉proto语句,可以简化程序和避免出错。
3.4.2 参数传递和堆栈平衡 了解了子程序的定义方法后,让我们继续深入了解子程序的使用细节。在调用子程序时,参数的传递是通过堆栈进行的,也就是说,调用者把要传递给子程序的参数压入堆栈,子程序在堆栈中取出相应的值再使用,比如,如果要调用: SubRouting(Var1,Var2,Var3) 经过编译后的最终代码可能是(注意只是“可能”): push Var3 push Var2 push Var1 call SubRouting add esp,12 也就是说,调用者首先把参数压入堆栈,然后调用子程序,在完成后,由于堆栈中先前压入的数不再有用,调用者或者被调用者必须有一方把堆栈指针修正到调用前的状态,即堆栈的平衡。参数是最右边的先入堆栈还是最左边的先入堆栈、还有由调用者还是被调用者来修正堆栈都必须有个约定,不然就会产生错误的结果,这就是在上述文字中使用“可能”这两个字的原因。各种语言中调用子程序的约定是不同的,所以在proc以及proto语句的语言属性中确定语言类型后,编译器才可能将invoke伪指令翻译成正确的样子,不同语言的不同点如表3.4所示。 ![]()
表3.4 不同语言调用方式的差别
为了了解编译器对不同类型子程序的处理方式,先来看一段源程序: ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Sub1 proc C _Var1,_Var2 mov eax,_Var1 mov ebx,_Var2 ret Sub1 endp ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Sub2 proc PASCAL _Var1,_Var2 mov eax,_Var1 mov ebx,_Var2 ret Sub2 endp ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Sub3 proc _Var1,_Var2 mov eax,_Var1 mov ebx,_Var2 ret b3 endp ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> … invoke Sub1,1,2 invoke Sub2,1,2 invoke Sub3,1,2 编译后再进行反汇编,看编译器是如何转换处理不同类型的子程序的: ; 这里是Sub1 - C类型 :00401000 55 push ebp :00401001 8BEC mov ebp, esp :00401003 8B4508 mov eax, dword ptr [ebp+08] :00401006 8B5D0C mov ebx, dword ptr [ebp+0C] :00401009 C9 leave :0040100A C3 ret ; 这里是Sub2 - PASCAL类型 :0040100B 55 push ebp :0040100C 8BEC mov ebp, esp :0040100E 8B450C mov eax, dword ptr [ebp+0C] :00401011 8B5D08 mov ebx, dword ptr [ebp+08] :00401014 C9 leave :00401015 C20800 ret 0008 ; 这里是Sub3 — StdCall类型 :00401018 55 push ebp :00401019 8BEC mov ebp, esp :0040101B 8B4508 mov eax, dword ptr [ebp+08] :0040101E 8B5D0C mov ebx, dword ptr [ebp+0C] :00401021 C9 leave :00401022 C20800 ret 0008 … ; 这里是invoke Sub1,1,2 — C类型 :00401025 6A02 push 00000002 :00401027 6A01 push 00000001 :00401029 E8D2FFFFFF call 00401000 :0040102E 83C408 add esp, 00000008 ; 这里是invoke Sub2,1,2 — PASCAL类型 :00401031 6A01 push 00000001 :00401033 6A02 push 00000002 :00401035 E8D1FFFFFF call 0040100B ; 这里是invoke Sub3,1,2 — StdCall类型 :0040103A 6A02 push 00000002 :0040103C 6A01 push 00000001 :0040103E E8D5FFFFFF call 00401018 可以清楚地看到,在参数入栈顺序上,C类型和StdCall类型是先把右边的参数先压入堆栈,而PASCAL类型是先把左边的参数压入堆栈。在堆栈平衡上,C类型是在调用者在使用call指令完成后,自行用add esp,8指令把8个字节的参数空间清除,而PASCAL和StdCall的调用者则不管这个事情,堆栈平衡的事情是由子程序用ret 8来实现的,ret指令后面加一个操作数表示在ret后把堆栈指针esp加上操作数,完成的是同样的功能。 Win32约定的类型是StdCall,所以在程序中调用子程序或系统API后,不必自己来平衡堆栈,免去了很多麻烦。 存取参数和局部变量都是通过堆栈来定义的,所以参数的存取也是通过ebp做指针来完成的。在探讨局部变量的时候,已经就没有参数的情况下ebp指针和局部变量的对应关系做了分析,现在来分析一下ebp指针和参数之间的对应关系,注意,这里是以Win32中的StdCall为例,不同的语言类型,指针的顺序可能是不同的。 假定在一个子程序中有两个参数,主程序调用时在 push 第一个参数前的堆栈指针esp为X,那么压入两个参数后的esp为X-8,程序开始执行call指令,call指令把返回地址压入堆栈,这时候eps为X-C,接下去是子程序中用push ebp来保存ebp的值,esp变为X-10,再执行一句mov ebp,esp,就可以开始用ebp存取参数和局部变量了,图3.4说明了这个过程。 图3.4 ebp指针、参数和局部变量的关系 在源程序中,由于参数、局部变量和ebp的关系是由编译器自动维护的,所以读者不必关心它们的具体关系,但到了用Soft-ICE等工具来分析其他软件的时候,遇到调用子程序的时候一定要先看清楚它们之间的类型差别。 在子程序中使用参数,可以使用与存取局部变量同样的方法,因为这两者的构造原理几乎一模一样,所以,在子程序中有invoke语句时,如果要用到输入参数的地址当做invoke的参数,同样要遵循局部变量的使用方式,不能用offset伪操作符,只能用addr来完成。同样,所有对局部变量使用的限制几乎都可以适用于参数。 | |
来源:电子工业出版社 作者:罗云彬
|
使用MASM-使用子程序(参数传递和堆栈平衡)
最新推荐文章于 2024-08-09 14:58:08 发布