[学习心得]win32汇编子程序的参数传递 By hslwq(于2007-9-12发表)

  学过C,最近在认真学习Win32汇编。几年前就开始逛这个汇编小站了,最近因为学习的缘故,所以来的频繁了一些。我现在学习汇编,完全是一个人过河的模式,身边没有人可以交流,学得非常辛苦,所以网络与这个站点给了我很大的帮助,真心的感谢Aogo。
  Aogo的这个站点,人气越来越多,高手也多,这是一篇初级的学习心得,发表出来,不免心虚,然终于鼓足勇气拿出来,是基于如下原因:
1、网上及书上对这个问题的阐述不是很详细,一些新手可能会问:如何象C语言那样使用指针参数?高手可能会回答:变量地址作为参数即可。但事实上,对新手来说,子程序如何利用这个传过来的参数控制变量也是难点。
2、网站上面向菜鸟的文章不多,或者已经被贴子淹没,学习心得也不多,而我现在需要的是学习心得的交流,所以拿出来,希望起一个抛砖引玉的作用--象我一样正在探索的人,都把学习中的心得拿出来,让大家都有进步,少走弯路。而高手们在成为高手之前,想必也解决过不解与困惑,如果能写出来,对菜鸟们是很大的帮助。总之,热心是汇编队伍壮大的动力。
3、发现一个人的学习是一件很痛苦的事,所以希望通过此贴,结交一些志同道合的朋友,可以共同学习进步(MYQQ:48112920)。
4、在发贴之前,特地去看了发贴规则,发现发表心得是受鼓励的行为。(以下言归正传)

  在C语言中,交换两个变量值的子程序中的两个参数必须设置为指针才能保证两个变量值正确交换,而在汇编语言中,我们又该如何实现指针参数的传递与处理?要回答这个问题,必须对汇编子程序的参数传递有较为深入的理解。现举例说明,例程如下:
.386
.model flat,stdcall
option casemap:none

include windows.inc
include user32.inc
include kernel32.inc
include gdi32.inc

includelib user32.lib
includelib kernel32.lib
includelib gdi32.lib

LPVAR        typedef PTR        DWORD

swap proto        a:LPVAR,b:LPVAR
.const
szCaption        db 'Message',0
szFormat                db 'a=%d,b=%d',0

.data
szBuffer                db 128 dup (?)

.data?
dwX        dd        ?
dwY        dd ?

.code        
start:
mov        dwX,20
mov        dwY,30
invoke        wsprintf,addr szBuffer,addr szFormat,dwX,dwY
invoke        MessageBox,NULL,addr szBuffer,addr szCaption,MB_OK
invoke        swap,addr dwX,addr dwY
invoke        wsprintf,addr szBuffer,addr szFormat,dwX,dwY
invoke        MessageBox,NULL,addr szBuffer,addr szCaption,MB_OK
invoke        ExitProcess,NULL

swap proc        a:LPVAR,b:LPVAR
pushad
mov eax,a
mov ebx,b
mov        ecx,DWORD ptr [eax]
mov        edx,DWORD ptr [ebx]
mov        DWORD ptr [eax],edx
mov        DWORD ptr [ebx],ecx
popad
ret
swap endp
end start
分析:
1、指针变量的定义:
在此程序中,我们定义了一个指针类型LPVAR,用于指向DWORD变量的地址:
LPVAR        typedef PTR        DWORD
为了程序可读性,我们也可以如下定义:
VAR typedef DWORD
LPVAR        typedef PTR        VAR
2、参数与局部变量的入栈顺序
    存取参数和局部变量都是通过堆栈来进行的,所以参数的存取也是通过esp与ebp指针来完成的。在例程中定义了一个子程序:swap 用于实现两个变量值的交换,其中的两个参数是指向VAR变量的指针,可以试验,任何DWORD类型的变量作为swap的实参将不会报错,所以Win32中所有的指针类型都是DWORD型,即所有变量地址是32位的。
    当程序调用swap函数时,即编译语句invoke        swap,addr dwX,addr dwY
时,必须先把两个参数入栈,即展成如下形式:
00401055  |.  68 A0414000   PUSH swap.004041A0;参数2入栈
0040105A  |.  68 9C414000   PUSH swap.0040419C;参数1入栈
0040105F  |.  E8 38000000   CALL swap.0040109C;调用swap子进程
00401064  |.  FF35 A0414000 PUSH DWORD PTR DS:[4041A0];CALL语句的下一条语句地址(即00401064)将是swap子进程的返回地址。
假设调用swap函数之前堆栈指针esp如下:
ESP 0012FFC4
EBP 0012FFF0
运行PUSH swap.004041A0后ESP为:0012FFC0
运行PUSH swap.0040419C后ESP为:0012FFBC
运行CALL swap.0040109C语句后,call指令把返回地址入栈,此时栈顶地址为:
ESP  0012FFB8
转入swap子程序后,代码如下:
0040109C  /$  55            PUSH EBP
0040109D  |.  8BEC          MOV EBP,ESP
0040109F  |.  60            PUSHAD
004010A0  |.  8B45 08       MOV EAX,DWORD PTR SS:[EBP+8]
004010A3  |.  8B5D 0C       MOV EBX,DWORD PTR SS:[EBP+C]
004010A6  |.  8B08          MOV ECX,DWORD PTR DS:[EAX]
004010A8  |.  8B13          MOV EDX,DWORD PTR DS:[EBX]
004010AA  |.  8910          MOV DWORD PTR DS:[EAX],EDX
004010AC  |.  890B          MOV DWORD PTR DS:[EBX],ECX
004010AE  |.  61            POPAD
004010AF  |.  C9            LEAVE
004010B0  \.  C2 0800       RETN 8
运行:
0040109C  /$  55            PUSH EBP
0040109D  |.  8BEC          MOV EBP,ESP之后,ESP与EBP如下:
ESP 0012FFB4
EBP 0012FFB4
参数传递前后ESP与EBP变化如下:
ESP指向                                 堆栈内容            参数与EBP相对地址
0012FFC4(起始堆栈,记为X)
X-04h--------->               参数2入栈                   Ebp+0ch
X-08h--------->              参数1入栈                   Ebp+08h
X-0ch--------->              返回地址(00401064)入栈     Ebp+04h
X-10h--------->               EBP入栈                <-mov ebp,esp
为局部变量分配堆栈空间
结论:
1、在进入子程序之前,参数入栈,入栈顺序:从右到左,紧接着CALL语句把返回地址入栈再跳转进入子程序执行,而子程序马上把EBP入栈用于保护子程序返回时的堆栈指针。调用完后,由子程序负责清除参数占用的堆栈空间(格式:RET n,本例中函数返回时指令retn 8表示返回后把esp指针加上8,此时ESP=0012FFC4,刚好等于调用之有ESP的值),即返回之前的指令与对应堆栈情况:
LEAVE
RETN 8
等价于:
mov esp,ebp        esp--->X-10h
Pop ebp                Esp--------->X-0ch
Ret      8               Esp---------->X-08h(返回地址出栈),esp+08h(丢弃参数)
结果
ESP————>X
2、进入子程序后,子程序先把EBP入栈,并把当前ESP保存在EBP中用于返回时恢复堆栈。由此可见:在子程序中,EBP起到了保存原始ESP的作用,并随时用做存取局部变量与取参数的指针基址。
3、局部变量是子程序临时分配的堆栈空间,可以用ebp为基址进行访问。
试验与实践:
实践1、把上述例程中swap子程序改为用ebp存取参数,如下:
swap proca:LPVAR,b:LPVAR
pushad
mov eax,[ebp+0ch];取参数2
mov ebx,[ebp+8h];取参数1
mov ecx,DWORD ptr [eax]
mov edx,DWORD ptr [ebx]
mov DWORD ptr [eax],edx
mov DWORD ptr [ebx],ecx
popad
ret
实践2:对参数的传递方式与局部变量的理解有了深入的认识以后,我们就可以在程序设计中更为灵活地进行数据传递,比如,我们可以象C语言中的指针那样把变量地址作为参数传递,从而实现在子程序中直接访问或改变变量的值。有些时候,当子程序要返回的内容比较复杂,eax不能容纳时,我们也可以通过传递指针,直接返回变量地址。例程如下:
.386
.model flat,stdcall
option casemap:none

include        windows.inc
include         user32.inc
include         kernel32.inc

includelib        user32.lib
includelib kernel32.lib
TESTSTRUCT STRUCT
cPoint        POINT <>
r        DWORD        ?
TESTSTRUCT        ends
LPTESTSTRUCT typedef ptr TESTSTRUCT

_testProc        proto        lpstTest:LPTESTSTRUCT
.const
szCaption        db 'MessageBox',0
szFormatText1        db '变量地址:%08x',0
szFormatText2        db        '变量值:%5d,%5d,%5d',0
.data
szbuffer1        db 128 dup (?)
szbuffer2        db 128 dup (?)
.data?
stTest        TESTSTRUCT        <>

.code
start:
invoke        wsprintf,addr szbuffer1,addr szFormatText1,addr stTest
invoke        wsprintf,addr szbuffer2,addr szFormatText2,stTest.cPoint.x,stTest.cPoint.y,stTest.r
invoke        MessageBox,NULL,addr szbuffer2,offset szCaption,MB_OK
invoke        MessageBox,NULL,offset  szbuffer1,offset szCaption,MB_OK
invoke        _testProc,addr stTest
invoke        wsprintf,addr szbuffer2,addr szFormatText2,stTest.cPoint.x,stTest.cPoint.y,stTest.r
invoke        MessageBox,NULL,addr szbuffer2,offset szCaption,MB_OK
invoke ExitProcess,NULL
_testProc        proc        lpstTest:LPTESTSTRUCT
local @buf[256]:BYTE
pushad
invoke        wsprintf,addr @buf,offset szFormatText1,lpstTest
invoke        MessageBox,NULL,addr @buf,offset szCaption,MB_OK
mov        eax,lpstTest
mov        DWORD ptr [eax],1
mov        DWORD ptr [eax+4],2
mov        DWORD ptr [eax+8],3
popad
ret
_testProc        endp
end start
程序分析:
程序定义了一个记录类型及相应指针类型:
TESTSTRUCT STRUCT
cPoint        POINT <>
r        DWORD        ?
TESTSTRUCT        ends
LPTESTSTRUCT typedef ptr TESTSTRUCT
并定义了一个子程序:
_testProc        proto        lpstTest:LPTESTSTRUCT
子程序参数为指向记录的指针,功能为通过传递的指针初始化记录的值,并在主程序中重新输出。
实践3:获得运行期标号地址修正值,例程如下:
call FUNC_START
FUNC_START:
    pop ebx
    sub ebx, offset FUNC_START
    mov [ebp-xx], ebx
分析:如上所述,call 函数会将eip寄存器压入堆栈,而eip表示的是"下一条语句的地址,所以例程中的
pop ebx将把eip的内容放入ebp中即标号FUNC_START对应的地址。在这里, 当程序运行到"call FUNC_START"时, 它表示的是以标号"FUNC_START:"开始的"pop ebx"指令起始地址. 而另一方面, sub指令中的"offset FUNC_START", 在编译时, offset会被转成一个绝对地址. 这样,通过sub操作, 就获得了此段代码在编译期和运行期关于指令地址的修正值. 下面的这句: "mov [ebp-xx], ebx", 实际上只是锦上添花, 它把这个值保存在了某一个自定义的函数局部变量空间内, 以备后续语句方便引用.
接下来,可以用如下方式 对标号数据进行引用:
    mov eax, [ebp-xx]
    mov ebx, dword ptr [DATA_LABLE+eax]
    对于汇编函数中的此类代码进行这样的处理后, 此段二进制执行块就可以被放置在任意地方而不致因为对DATA_LABLE数据地址的错误引用造成程序错误.

  以上是对Win32子程序如何传递参数的体会,重点讨论了在子程序中如何操作参数,实践三很有意思,参考了网上的文章,希望高手指导。

并不是所有的贴子都是原创,此时作者均指发表的人而不是文章的作者,作者会说明是否是转贴