MASM32汇编中关于栈的总结
关于函数调用中栈的总结
1.栈,是由高地址分配到低地址的。
32位程序中,寄存器ebp指向栈的起始位置,也就是栈的基地址(高地址)
寄存器esp指向栈的目前位置,也就是栈顶。
现在假设程序才运行,ebp和esp寄存器都是0x00190000h
接下来的指令是push 0x1800004H,那么ebp还是0x00190000h
因为push一个整数占用4字节,所以esp-4=0x0018fffcH
所以esp=0x0018FFFCH,
再来看看内存分配,这里以小端为例。
0x0018FFFCH | 0X0018FFFDH | 0X0018FFFEH | 0X18FFFFH |
---|---|---|---|
04 | 00 | 00 | 18 |
所以,esp减少,代表压入数据,esp增大,代表弹出数据
2.栈的应用
1.临时保存值。
这里以寄存器为例。
假定一开始eax的值为5
指令 | eax寄存器的值 |
---|---|
push eax | 5 |
inc eax | 6 |
dec eax | 5 |
2.保存函数调用的返回地址。
假设程序是顺序执行。
那么,在执行call指令的时候,会先往栈中push call指令往下一条语句的地址
3.传递函数参数。
调用一个有参数的函数,都是先push一些参数,然后再执行call指令。
4.存放函数中的局部变量。
OD跟踪一个函数执行时,往往能看见过程一开始,就是
sub esp,4这样的语句,这代表分配了4字节的空间给某个局部变量。
使用那个局部变量的时候,往往用的ebp-4来代表那个地址。
3.CALL指令的调用过程
1.将调用函数使用的参数入栈
2.将call指令往后需要执行的指令入栈,函数执行完毕后用到
3.保存原始ebp指针的值
4.为函数准备栈空间,ebp指向函数的栈的基地址
5.为函数中的局部变量开辟栈空间
6.执行函数语句
7.清理局部变量,恢复ebp指针,(leave指令)
8.返回执行call指令往后执行的指令,清理函数调用push的栈(retn)
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
.code
_add proc a:word,b:word
local @bl1:dword
ret
_add endp
start:
push 1
push 2
call _add
add eax,1
end start
这样的代码,我们编译链接,在OD中运行,查看字节码。
00401000 | 55 | push ebp |
00401001 | 8BEC | mov ebp,esp |
00401003 | 83C4 FC | add esp,FFFFFFFC |
00401006 | C745 FC 05000000 | mov dword ptr ss:[ebp-4],5 |
0040100D | C9 | leave |
0040100E | C2 0800 | ret 8 |
00401011 <my.EntryPoint> | 6A 01 | push 1 |
00401013 | 6A 02 | push 2 |
00401015 | E8 E6FFFFFF | call my.401000 |
0040101A | 83C0 01 | add eax,1 | |
接下来我们逐条解释 。
编写这8条即可保证堆栈平衡。比如调用函数前,栈里面有7个4字节的指针,运行到最后(add eax,1)的时候,栈里面就应该还是7个4字节的指针,并且数据不变。
(1)因为这个过程有两个参数,所以这里push两个参数,如果改成只push一个,那么执行完函数的时候,start函数里面,原先的栈就被破坏了,栈失衡可能会造成运行错误。
(2)大家使用OD跟着调试的时候,可以注意观察堆栈的数据,进入调用函数的时候,栈顶保存的就是call指令后面的那条指令的地址
(3)(4)(5)经常,调用过程的时候,
总是看见过程开头是这几句,就拿这里为例
00401000 | 55 | push ebp |
00401001 | 8BEC | mov ebp,esp |
00401003 | 83C4 FC | add esp,FFFFFFFC |
00401006 | C745 FC 05000000 | mov dword ptr ss:[ebp-4],5 |
每个函数都有自己的栈空间,ebp指向的就是函数栈空间的起点,所以这里会先push ebp,
保存start里面的栈的基址。
mov ebp,esp。调整函数的栈的基址。为什么要这样,我们接着往下看。
add esp,FFFFFFC,也就是栈顶往下移动了4字节,也就是分配给@bl1的空间。
而机器码里面可是没有这个局部变量名称的,那么如何做到使用这个空间呢?
也就是这一句 mov dword ptr ss:[ebp-4],5 。对局部变量的操作,主要通过栈基址的偏移来操作
所以,调用函数,才需要保存原始栈基址,设置新的栈基址。
(6)这里没有涉及,很多时候,通过OD,我们能看到
pushad
xxxxxx
popad
这样的语句,也是利用栈来保存寄存器的值
(7)函数一开头就保存了start的栈基址,那么,为了维持堆栈平衡,函数结束的时候,就应该恢复栈基址。
这是通过leave指令实现的。
等价于
mov esp,ebp
pop ebp
回到start函数,栈的基地址恢复了,栈顶地址也恢复了,也就是回到了调用call指令以后,执行函数过程前的状态。
(8)最后一个ret 8,因为这个函数调用,占用8字节,所以会再弹出下一条指令地址后,再弹出8字节的数据。
至此,函数调用中的栈就完毕了。
4.栈溢出
且看代码:
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
.data
szText db 'HelloWorldPE',0
szText2 db 'Touch Me!',0
szShellCode dd 0fffffffh,0dddddddh,0040103ah,0
.code
_memCopy proc _lpSrc
local @buf[4]:byte
pushad
mov al,1
mov esi,_lpSrc
lea edi,@buf
.while al!=0
mov al,byte ptr [esi]
mov byte ptr [edi],al
inc esi
inc edi
.endw
popad
ret
_memCopy endp
start:
invoke _memCopy,addr szShellCode
invoke MessageBox,NULL,offset szText,NULL,MB_OK
invoke MessageBox,NULL,offset szText2,NULL,MB_OK
invoke ExitProcess,NULL
end start
看代码可能会以为弹出两个信息框,实际上只会弹出一个。这里的shellCode是经过专门设计的。
在_memCopy过程中,edi指向了栈中分配的局部变量的地址,随后又不断越界修改,改变了栈空间内存里面的数据,最重要的,就是改变了call指令返回以后需要执行的指令的地址,这个可以利用OD调试跟踪运行。