[转载]Win32Asm子程序的参数传递
昨天晚上大约23:55分的时候小弟在进行网上冲浪,就去aogo的论坛看了看(不知道站长和tangptr前辈有没有听说过aogo这个人),发现一篇帖子,是前辈hslwq在07年发的。阅读完了以后感觉很好,小弟就想自己先消化一下,然后转载一下,巩固自己的知识以外也希望更多人可以从hslwq前辈的帖子中受益。
帖子原址:http://www.aogosoft.com/bbs/mixpage.asp?mode=viewoktext&fileid=74936
我对原帖进行了一些改动,首先就是帖子的源代码的格式的改动,我将原帖中的代码在RadAsm中又敲了一遍,格式用的是罗云彬在win32汇编语言书中的格式
其次就是对帖子的源代码进行了注释,并且删除了一些冗余的代码,比如函数声明等。再者在进行分析程序的时候自己在IDA中重新反汇编分析一遍,然后
对反汇编的代码进行了注释,想要大家看的更加清楚,同时以便自己来检查原帖中的代码的正确性,毕竟实践出真知。
下面进行正文:
本文主要讲解win32汇编中的子程序的传址调用,这点和C语言类似。win32汇编中子程序通过传址调用可以对实参进行修改。程序主要是通过很经典的交换两个数的子程序来对
实参中的两个数值进行交换。
第一个源码source.asm以及反汇编的source-ida.asm是本帖和原帖的精髓所在:
source.asm
.386
.model flat, stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib
;自定义一个dword类型(指针变量),与C语言中的typedef类似
LPVAR typedef ptr dword
;定义常量字符串
.const
szCaption db 'Message',0
szFormat db 'a = %d, b = %d',0 ;定义输出格式
;定义数据段
.data
szBuffer db 128 dup(?) ;用于存储输出结果
;定义两个dword变量,用于实验
.data?
dwX dd ?
dwY dd ?
;代码段
.code
;自定义的交换两个数值的函数,进行传址调用
_Swap proc a:LPVAR,b:LPVAR
pushad ;将所有的寄存器的数值压栈
mov eax,a
mov ebx,b
;交换数值
mov ecx,dword ptr
mov edx,dword ptr
mov dword ptr ,edx
mov dword ptr ,ecx
popad ;将所有的寄存器的数值弹栈
ret
_Swap endp
start:
mov dwX,20
mov dwY,30
invoke wsprintf,offset szBuffer,offset szFormat,dwX,dwY
invoke MessageBox,NULL,offset szBuffer,offset szCaption,MB_OK
invoke _Swap, offset dwX,offset dwY ;调用自定义的交换函数
invoke wsprintf,offset szBuffer,offset szFormat,dwX,dwY
invoke MessageBox,NULL,offset szBuffer,offset szCaption,MB_OK
invoke ExitProcess,NULL
end start
运行结果为:
同时IDA的反汇编分析了子程序和形参的入栈出栈和调用方式:
source-ida.asm
sub_401000 proc near
arg_0= dword ptr8 ;参数一入栈时候,栈顶指针的偏移量
arg_4= dword ptr0Ch ;参数二入栈时候,栈顶指针的偏移量
push ebp ;先将ebp入栈,保存ebp的原始值
mov ebp, esp ;将esp的值赋值给ebp,以后用ebp来进行栈顶指针的移动
pusha
mov eax, ;将第一个参数的地址赋值给eax
mov ebx, ;将第二个参数的地址赋值给ebx
mov ecx, ;将第一个参数地址所对应的数值赋值给ecx
mov edx, ;将第二个参数地址所对应的数值赋值给ecx
mov ,edx ;下面两句用于交换数值
mov ,ecx
popa
leave
retn 8 ;stdcall调用方式平衡堆栈用的,这里我们不予理会
sub_401000 endp
结论:
1、在进入子程序之前,参数入栈,入栈顺序:从右到左,紧接着CALL语句把返回地址入栈再跳转进入子程序执行,而子程序马上把EBP入栈用于保护子程序返回时的堆栈指针。调用完后,由子程序负责清除参数占用的堆栈空间(格式:RET n,本例中函数返回时指令retn 8表示返回后把esp指针加上8,此时ESP=0012FFC4,刚好等于调用之有ESP的值)
2、进入子程序后,子程序先把EBP入栈,并把当前ESP保存在EBP中用于返回时恢复堆栈。由此可见:在子程序中,EBP起到了保存原始ESP的作用,并随时用做存取局部变量与取参数的指针基址。
3、局部变量是子程序临时分配的堆栈空间,可以用ebp为基址进行访问。
下面按照原帖的步骤进行三个小实验:
实践1:改动代码部分,用ebp来存取参数。 实际上这个实验很好理解,能看懂上图中的IDA的反汇编就可以理解这个了,具体ebp指针,参数和局部变量的关系以及堆栈的生长方向在罗云彬win32汇编语言程序设计第三版的第78页有详细描述,在此不再赘述,代码的改动部分如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 实践1,改动代码部分,用ebp来存取参数
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Swap proc a:LPVAR,b:LPVAR
pushad
mov eax,
mov ebx,
mov ecx,dword ptr
mov edx,dword ptr
mov dword ptr ,edx
mov dword ptr ,ecx
popad
ret
_Swap endp
运行结果同source.asm的结果
实践2:通过传递指针,直接返回变量地址
这个实验的程序较上一个程序在数据结构上稍微有些复杂,就是运用了结构体,但是实际上和C语言的结构体指针作参数来传递给子函数一样,这里我也加了很多注释,大家应该可以看的很明白。
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 实践二:通过传递指针,直接返回变量地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.386
.model flat, stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;声明一个结构体TESTSTRUCT
TESTSTRUCT struct
cPoint POINT <> ;定义一个POINT结构体
r dword ?
TESTSTRUCT ends
;定义指向TESTSTRUCT结构体的结构体指针
LPTESTSTRUCT typedef ptr TESTSTRUCT
.const
szCaption db 'MessageBox',0
szFormatText1 db '变量地址:%08x',0
szFormatText2 db '变量值:%5d,%5d,%5d',0
.data
szbuffer1 db 128 dup(?)
szbuffer2 db 128 dup(?)
stTest TESTSTRUCT <> ;定义了一个TESTSTRUCT结构体变量stTest
.code
;子程序参数为指向记录的指针
;功能为通过传递的指针初始化记录的值,并在主程序中重新输出
_testProc proc lpstTest:LPTESTSTRUCT
LOCAL @buf:byte
pushad
invoke wsprintf,addr @buf,offset szFormatText1,lpstTest ;输出形参的值,即为实参的地址
invoke MessageBox,NULL,addr @buf,offset szCaption,MB_OK
mov eax,lpstTest ;将结构体的地址赋给eax
mov dword ptr ,1 ;给结构体中的各个成员变量赋初值
mov dword ptr ,2
mov dword ptr ,3
popad
ret
_testProc endp
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,offsetszbuffer1,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
end start
运行结果:
实践3:获得运行期标号地址修正值
这个实践说实话很明显是dos下16位汇编经常用的招式,感觉在win32汇编中不是很实用,原帖在此处也是作为一个课外话题
原帖关于此处内容如下:
call FUNC_START
FUNC_START:
pop ebx
sub ebx, offset FUNC_START
mov , ebx
分析:如上所述,call 函数会将eip寄存器压入堆栈,而eip表示的是"下一条语句的地址,所以例程中的
pop ebx将把eip的内容放入ebp中即标号FUNC_START对应的地址。在这里, 当程序运行到"call FUNC_START"时, 它表示的是以标号"FUNC_START:"开始的"pop ebx"指令起始地址. 而另一方面, sub指令中的"offset FUNC_START", 在编译时, offset会被转成一个绝对地址. 这样,通过sub操作, 就获得了此段代码在编译期和运行期关于指令地址的修正值. 下面的这句: "mov , ebx", 实际上只是锦上添花, 它把这个值保存在了某一个自定义的函数局部变量空间内, 以备后续语句方便引用.
接下来,可以用如下方式 对标号数据进行引用:
mov eax,
mov ebx, dword ptr
对于汇编函数中的此类代码进行这样的处理后, 此段二进制执行块就可以被放置在任意地方而不致因为对DATA_LABLE数据地址的错误引用造成程序错误.
最后再次感谢hslwq前辈这么精彩的文章供小弟研读,同时希望各位大佬多多指针文章中的错误,共勉。
64位没啥适合独立开发的汇编编译器了,写起来太累人了 对于混合编译来说了解约定基本够用了 不适用于在Win64下写64位汇编程序(MASM64的invoke很骚,不能像MASM32那样玩。初学汇编的话建议少玩汇编器提供的宏能力,对学习调用约定没好处)
关于Win64可以参考https://www.0xaa55.com/forum.php?mod=viewthread&tid=16875 tangptr@126.com 发表于 2018-11-3 09:02
不适用于在Win64下写64位汇编程序(MASM64的invoke很骚,不能像MASM32那样玩。初学汇编的话建议少玩汇编器 ...
哦哦,谢谢tangptr大佬指导,Win64的那篇帖子好屌
页:
[1]