|
|
|
|
在 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 ...
|
| | |
|
|