在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 语句。也就是说,<b>在 api 或子程序中,最右边的参数先入堆栈,然后子程序在返回的时候负责校正堆栈</b>,举例说明,如果我们要调用 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
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiZpdmLrNWY0N3LcJzMul2dvwVbhJ3ZvJHcvwlbj5SbvNmLtNXYyMjbpdnL3d3dvw1LcpDc0RHaiojIsJye.gif)
下
面要讲的是子程序如何存取参数,因为缺省对堆栈操作的寄存器有 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
leave
好
了,说到这儿,参数传递的原理也应该将清楚了,还要最后说的是,在使用 masm32 编 win32 汇编程序的时候,我们并不需要记住 [ebp +
xx] 等麻烦的地址,或自己计算局部变量需要预留的堆栈空间,还有在 ret 时计算要加上的数值,masm32 的宏指令都已经把这些做好了,如在
masm32 中,上面的程序只要写成为:
myproc proc var1,var2
local lvar1,lvar2
mov eax,var1
sub eax,var2
ret
编
译器会自动的在 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
就行了,如何,是不是很简单啊?不过我可苦了,这篇文章整整花了我一个晚上 ... ##%$^&(*&^(*&(^&(*