在Win32汇编中,我们经常要和Api 打交道,另外也会常常使用自己编制的类似于Api 的带参数的子程序,本文要讲述的是在子程序调用的过程中进行参数传递的概念和分析。一般在程序中,参数的传递是通过堆栈进行的,也就是说,调用者把要传递 给子程序(或者被调用者)的参数压入堆栈,子程序在堆栈取出相应的值再使用,比如说,如果你要调用 SubRouting(Var1,Var2,Var3),编译后的最终代码可能是
push Var3
push Var2
push Var1
call SubRouting
add esp,12
也 就是说,调用者首先把参数压入堆栈,然后调用子程序,在完成后,由于堆栈中先前压入的数不再有用,调用者或者被调用者必须有一方把堆栈指针修正到调用前的 状态。参数是最右边的先入堆栈还是最左边的先入堆栈、还有由调用者还是被调用者来修正堆栈都必须有个约定,不然就会产生不正确的结果,这就是我在前面使用 “可能”这两个字的原因:各种语言中调用子程序的约定是不同的,它们的不同点见下表:
C |
SysCall |
StdCall |
Basic |
Fortran |
Pascal |
|
参数从左到右 |
是 |
是 |
是 |
|||
参数从右到左 |
是 |
是 |
是 |
|||
调用者清除堆栈 |
是 |
|||||
允许使用:VARARG |
是 |
是 |
是 |
VARARG 表示参数的个数可以是不确定的,有一个例子就是 C 中的 printf 语句,在上表中,StdCall 的定义有个要说明的地方,就是如果StdCall 使用 :VARARG 时,是由调用者清除堆栈的,而在没有:VARARG时是由被调用者清除堆栈的。在Win32 汇编中,约定使用StdCall 方式,所以我们要在程序开始的时候使用 .model stdcall 语句。也就是说,在 API 或子程序中,最右边的参数先入堆栈,然后子程序在返回的时候负责校正堆栈,举例说明,如果我们要调用 MessageBox 这个 API,因为它的定义是 MessageBox(hWnd,lpText,lpCaption,UType) 所以在程序中要这样使用:
push MB_OK
push offset szCaption
push offset szText
push hWnd
call MessageBox
...
我 们不必在 API 返回的时候加上一句 add sp,4*4 来修正堆栈,因为这已经由 MessageBox 这个子程序做了。在 Windows API 中,唯一一个特殊的 API 是 wsprintf,这个 API 是 C 约定的,它的定义是 wsprintf(lpOut,lpFormat,Var1,Var2...),所以在使用时就要:
push 1111
push 2222
push 3333
push offset szFormat
push offset szOut
call wsprintf
add esp,4*5
下 面要讲的是子程序如何存取参数,因为缺省对堆栈操作的寄存器有 ESP 和 EBP,而 ESP是堆栈指针,无法暂借使用,所以一般使用 EBP 来存取堆栈,假定在一个调用中有两个参数,而且在 push 第一个参数前的堆栈指针 ESP 为 X,那么压入两个参数后的 ESP 为 X-8,程序开始执行 call 指令,call 指令把返回地址压入堆栈,这时候 ESP 为 X-C,这时已经在子程序中了,我们可以开始使用 EBP 来存取参数了,但为了在返回时恢复 EBP 的值,我们还是再需要一句 push ebp 来先保存 EBP 的值,这时 ESP 为 X-10,再执行一句 mov ebp,esp,根据上图可以看出,实际上这时候 [ebp + 8] 就是参数1,[ebp + c]就是参数2。另外,局部变量也是定义在堆栈中的,它们的位置一般放在 push ebp 保存的 EBP 数值的后面,局部变量1、2对应的地址分别是 [ebp-4]、[ebp-8],下面是一个典型的子程序,可以完成第一个参数减去第二个参数,它的定义是:
MyProc proto Var1,Var2 ;有两个参数
local lVar1,lVar2 ;有两个局部变量
注意,这里的两个 local 变量实际上没有被用到,只是为了演示用,具体实现的代码是:
MyProc proc
push ebp
mov ebp,esp
sub esp,8
mov eax,dword ptr [ebp + 8]
sub eax,dword ptr [ebp + c]
add esp,8
pop ebp
ret 8
MyProc endp
现在对这个子程序分析一下:
push ebp/mov ebp,esp 是例行的保存和设置 EBP 的代码;
sub esp,8 在堆栈中留出两个局部变量的空间;
mov /add 语句完成相加;
add esp,8 修正两个局部变量使用的堆栈;
ret 8 修正两个参数使用的堆栈,相当于 ret / add esp,8 两句代码的效果。
可以看出,这是一个标准的 Stdcall 约定的子程序,使用时最后一个参数先入堆栈,返回时由子程序进行堆栈修正。当然,这个子程序为了演示执行过程,使用了手工保存 ebp 并设置局部变量的方法,实际上,386 处理器有两条专用的指令是完成这个功能用的,那就是 Enter 和 Leave,Enter 语句的作用就是 push ebp/mov ebp,esp/sub esp,xxx,这个 xxx 就是 Enter 的,Leave 则完成 add esp,xxx/pop ebp 的功能,所以上面的程序可以改成:
MyPorc proc
enter 8,0
mov eax,dword ptr [ebp + 8]
sub eax,dword ptr [ebp + c]
leave
ret 8
MyProc endp
好 了,说到这儿,参数传递的原理也应该将清楚了,还要最后说的是,在使用 Masm32 编 Win32 汇编程序的时候,我们并不需要记住 [ebp + xx] 等麻烦的地址,或自己计算局部变量需要预留的堆栈空间,还有在 ret 时计算要加上的数值,Masm32 的宏指令都已经把这些做好了,如在 Masm32 中,上面的程序只要写成为:
MyProc proc Var1,Var2
local lVar1,lVar2
mov eax,Var1
sub eax,Var2
ret
MyProc endp
编 译器会自动的在 mov eax,Var1 前面插上一句 Enter 语句,它的参数会根据 local 定义的局部变量的多少自动指定,在 ret 前会自动加上一句 Leave,同样,编译器会根据参数的多少把 ret 替换成 ret xxx,把 mov eax,Var1 换成 mov eax,dword ptr [ebp + 8] 等等。
最后是使用 Masm32 的 invoke 宏指令,在前面可以看到,调用带参数的子程序时,我们需要用 push 把参数压入堆栈,如果不小心把参数个数搞错了,就会使堆栈不平衡,从而使程序从堆栈中取出错误的返回地址引起不可预料的后果,所以有必要有一条语句来完成 自动检验的任务,invoke 就是这样的语句,实际上,它是自动 push 所有参数,检测参数个数、类型是否正确,并使用 call 来调用的一个宏指令,对于上面的 push/push/call MyProc 的指令,可以用一条指令完成就是:
invoke MyProc,Var1,Var2
当然,当程序编译好以后你去看机器码会发现它被正确地换成了同样的 push/push/call 指令。但是,在使用 invoke 之前,为了让它进行正确的参数检验,你需要对函数进行申明,就象在 C 中一样,申明的语句是:
MyProc proto :DWORD,:DWORD
语 句中 proto 是关键字,表示申明,:DWORD 表示参数的类型是 double word 类型的,有几个就表示有几个参数,在 Win32 中参数都是 double word 型的,申明语句要写在 invoke 之前,所以我们一般把它包括在 include 文件中,好了,综合一下,在 Masm32 中使用一个带参数的子程序或者 Api ,我们只需用:
...
MyProc proto :dword,:dword
...
.data
x dd ?
y dd ?
dwResult dd ?
...
mov x,1
mov y,2
invoke MyProc x,y
mov dwResult,eax
...
就行了,如何,是不是很简单啊?不过我可苦了,这篇文章整整花了我一个晚上 ... ##%$^&(*&^(*&(^&(*
...