汇编中参数的传递和堆栈修正【转载】

简介:

 在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
   ...
就行了,如何,是不是很简单啊?不过我可苦了,这篇文章整整花了我一个晚上 ... ##%$^&(*&^(*&(^&(*
   ...

相关文章
|
4月前
|
存储
hyengine 寄存器问题之传递参数和接收返回值如何解决
hyengine 寄存器问题之传递参数和接收返回值如何解决
|
6月前
|
存储
函数调用与返回的相关指令
理解函数调用与返回的相关汇编指令及递归函数
在调用一个函数时传递了一个参数,但该函数定义中并未接受任何参数
在调用一个函数时传递了一个参数,但该函数定义中并未接受任何参数
125 2
|
7月前
|
编译器
fir原始崩溃栈解析
fir原始崩溃栈解析
31 0
|
7月前
|
存储 Serverless Python
函数调用的过程
函数调用的过程
97 0
|
7月前
|
C语言
C语言函数传递了指针,值没有被修改的原因及解决方法
C语言函数中传递了指针作为参数,确切来说是传递了指向变量的内存地址作为参数,可经过函数内的修改之后,该指针指向的变量的值为什么不会被修改?就像下方这个函数:
121 1
|
自然语言处理 程序员 C语言
C语言——函数(上)分类、参数、调用。
C语言——函数(上)分类、参数、调用。
顺序堆栈和链式堆栈的实现,用一个数组实现两个堆栈的例子
顺序堆栈和链式堆栈的实现,用一个数组实现两个堆栈的例子
定义函数,并用指针交换两个变量内容(正确版和错误版+错误原因)
定义函数,并用指针交换两个变量内容(正确版和错误版+错误原因)
97 0
定义函数,并用指针交换两个变量内容(正确版和错误版+错误原因)
|
存储 网络协议 C语言
【CSAPP】x86-64的机器代码和原始的C代码差别巨大,一些常在C语言中隐藏的处理器状态
【CSAPP】x86-64的机器代码和原始的C代码差别巨大,一些常在C语言中隐藏的处理器状态
85 0