- UID
- 3808
- 精华
- 积分
- 1480
- 威望
- 点
- 宅币
- 个
- 贡献
- 次
- 宅之契约
- 份
- 最后登录
- 1970-1-1
- 在线时间
- 小时
|
昨天晚上大约23:55分的时候小弟在进行网上冲浪,就去aogo的论坛看了看(不知道站长和tangptr前辈有没有听说过aogo这个人),发现一篇帖子,是前辈hslwq在07年发的。
阅读完了以后感觉很好,小弟就想自己先消化一下,然后转载一下,巩固自己的知识以外也希望更多人可以从hslwq前辈的帖子中受益。
帖子原址:http://www.aogosoft.com/bbs/mixp ... xt&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 [eax]
- mov edx,dword ptr [ebx]
- mov dword ptr [eax],edx
- mov dword ptr [ebx],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 ptr 8 ;参数一入栈时候,栈顶指针的偏移量
- arg_4= dword ptr 0Ch ;参数二入栈时候,栈顶指针的偏移量
- push ebp ;先将ebp入栈,保存ebp的原始值
- mov ebp, esp ;将esp的值赋值给ebp,以后用ebp来进行栈顶指针的移动
- pusha
- mov eax, [ebp+arg_0] ;将第一个参数的地址赋值给eax
- mov ebx, [ebp+arg_4] ;将第二个参数的地址赋值给ebx
- mov ecx, [eax] ;将第一个参数地址所对应的数值赋值给ecx
- mov edx, [ebx] ;将第二个参数地址所对应的数值赋值给ecx
- mov [eax],edx ;下面两句用于交换数值
- mov [ebx],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,[ebp+08h]
- mov ebx,[ebp+0ch]
-
- mov ecx,dword ptr [eax]
- mov edx,dword ptr [ebx]
- mov dword ptr [eax],edx
- mov dword ptr [ebx],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[256]: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 [eax],1 ;给结构体中的各个成员变量赋初值
- mov dword ptr [eax+4],2
- mov dword ptr [eax+8],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,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
- end start
复制代码
运行结果:
实践3:获得运行期标号地址修正值
这个实践说实话很明显是dos下16位汇编经常用的招式,感觉在win32汇编中不是很实用,原帖在此处也是作为一个课外话题
- 原帖关于此处内容如下:
- 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数据地址的错误引用造成程序错误.
复制代码
最后再次感谢hslwq前辈这么精彩的文章供小弟研读,同时希望各位大佬多多指针文章中的错误,共勉。
|
|