watermelon 发表于 2018-11-2 17:46:17

[转载]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前辈这么精彩的文章供小弟研读,同时希望各位大佬多多指针文章中的错误,共勉。




Ayala 发表于 2018-11-3 14:30:35

64位没啥适合独立开发的汇编编译器了,写起来太累人了 对于混合编译来说了解约定基本够用了

唐凌 发表于 2018-11-3 09:02:32

不适用于在Win64下写64位汇编程序(MASM64的invoke很骚,不能像MASM32那样玩。初学汇编的话建议少玩汇编器提供的宏能力,对学习调用约定没好处)
关于Win64可以参考https://www.0xaa55.com/forum.php?mod=viewthread&tid=16875

watermelon 发表于 2018-11-3 11:25:48

tangptr@126.com 发表于 2018-11-3 09:02
不适用于在Win64下写64位汇编程序(MASM64的invoke很骚,不能像MASM32那样玩。初学汇编的话建议少玩汇编器 ...

哦哦,谢谢tangptr大佬指导,Win64的那篇帖子好屌
页: [1]
查看完整版本: [转载]Win32Asm子程序的参数传递