函数调用时堆栈框架
2016-04-13 13:57
211 查看
http://blog.csdn.net/candycat1992/article/details/8130508
总体看来,构建一个堆栈框架包含了以下几个步骤:
如果要调用的函数有参数,将参数压入堆栈;
用call指令调用子程序;
此时子程序开始,将ebp寄存器压入栈:push ebp;
将ebp的值设为esp(只是为了方便以后访问参数和局部变量):mov ebp, esp;
若有局部变量,将esp的值减去相应的值。假设我们有3个DWORD类型的局部变量,则:sub esp, 12;
若有需要保存的寄存器,将要保存的寄存器压栈。
完成上述各个步骤后,堆栈的情况如下图:
从上图可以看出来,若要访问第一个被压入的参数,假设将第一个参数的值放到eax中,我们可以使用:
[plain] view
plain copy
print?
mov eax, [ebp + 8]
因为call指令会自动将返回地址压入堆栈,因此紧邻ebp上方的堆栈值不是参数,所以最近的参数地址是ebp + 8,而不是ebp + 4。访问其他参数类似,如[ebp + 12]等等。
在运行完函数的基本代码后,返回前我们需要清理堆栈以使函数能够正确返回,并恢复保存的寄存器值,还要警惕内存泄露问题。
清理堆栈的步骤如下:
将之前保存的寄存器值以相反的顺序弹出堆栈;
将esp的值设为ebp的值,以销毁局部变量:mov esp, ebp;
将原ebp的值弹出堆栈:pop ebp;
到了这里,esp的值已经指向了函数正确的返回地址。此时,若直接调用ret指令是可以达到返回调用函数的目的的。但我们可以发现之前被压入的参数并没有得到处理,仍然存在在堆栈中,当我们继续运行代码时,也没有人会为之前的函数”擦屁股“,即造成了内存泄露。解决这个问题有两个方法:
一个简单的方法是在call指令后面紧跟一条add指令,将esp的值指向一个正确的地址。例如,如果我们之前压入了3个参数,那么应运行:add esp, 12;
另一个更好的方法是是使用STDCALL调用规定,即修改子程序代码的ret指令。如对于上面的例子,应改写ret为:ret 12。
显然第二个方法更符合我们的习惯,因为”自己的事情自己做“,谁的函数谁的参数自己处理。
这里再补充一下各种调用规定的基本内容。
所有参数按照从右到左压入堆栈,由被调用的子程序清理堆栈
参数也是从右到左压入堆栈,但由调用者清理堆栈。
顾名思义,_fastcall的目的主要是为了更快的调用函数。它主要依靠寄存器传递参数,剩下的参数依然按照从右到左的顺序压入堆栈,并由被调用的子程序清理堆栈。
总结一下,汇编中一个子程序的代码长得像下面这个样子:
[plain] view
plain copy
print?
push ebp ;保存ebp
mov ebp, esp ;将ebp设为当前esp值
sub esp, 4*局部变量个数 ;局部变量
push eax ;将寄存器压栈
push esi
……
mov eax, [ebp + 8] ;得到第一个参数
mov edi, [ebp + 12] ;得到第二个参数
……
pop esi ;恢复寄存器值
pop eax
mov esp, ebp ;销毁局部变量
pop ebp ;恢复ebp的值
ret 4*被压入参数个数
创建堆栈框架
总体看来,构建一个堆栈框架包含了以下几个步骤:如果要调用的函数有参数,将参数压入堆栈;
用call指令调用子程序;
此时子程序开始,将ebp寄存器压入栈:push ebp;
将ebp的值设为esp(只是为了方便以后访问参数和局部变量):mov ebp, esp;
若有局部变量,将esp的值减去相应的值。假设我们有3个DWORD类型的局部变量,则:sub esp, 12;
若有需要保存的寄存器,将要保存的寄存器压栈。
完成上述各个步骤后,堆栈的情况如下图:
访问堆栈参数
从上图可以看出来,若要访问第一个被压入的参数,假设将第一个参数的值放到eax中,我们可以使用:[plain] view
plain copy
print?
mov eax, [ebp + 8]
因为call指令会自动将返回地址压入堆栈,因此紧邻ebp上方的堆栈值不是参数,所以最近的参数地址是ebp + 8,而不是ebp + 4。访问其他参数类似,如[ebp + 12]等等。
清理堆栈
在运行完函数的基本代码后,返回前我们需要清理堆栈以使函数能够正确返回,并恢复保存的寄存器值,还要警惕内存泄露问题。清理堆栈的步骤如下:
将之前保存的寄存器值以相反的顺序弹出堆栈;
将esp的值设为ebp的值,以销毁局部变量:mov esp, ebp;
将原ebp的值弹出堆栈:pop ebp;
到了这里,esp的值已经指向了函数正确的返回地址。此时,若直接调用ret指令是可以达到返回调用函数的目的的。但我们可以发现之前被压入的参数并没有得到处理,仍然存在在堆栈中,当我们继续运行代码时,也没有人会为之前的函数”擦屁股“,即造成了内存泄露。解决这个问题有两个方法:
一个简单的方法是在call指令后面紧跟一条add指令,将esp的值指向一个正确的地址。例如,如果我们之前压入了3个参数,那么应运行:add esp, 12;
另一个更好的方法是是使用STDCALL调用规定,即修改子程序代码的ret指令。如对于上面的例子,应改写ret为:ret 12。
显然第二个方法更符合我们的习惯,因为”自己的事情自己做“,谁的函数谁的参数自己处理。
函数调用约定
这里再补充一下各种调用规定的基本内容。
_stdcall调用约定
所有参数按照从右到左压入堆栈,由被调用的子程序清理堆栈
_cdecl调用约定(The C default calling convention,C调用规定)
参数也是从右到左压入堆栈,但由调用者清理堆栈。
_fastcall调用约定
顾名思义,_fastcall的目的主要是为了更快的调用函数。它主要依靠寄存器传递参数,剩下的参数依然按照从右到左的顺序压入堆栈,并由被调用的子程序清理堆栈。
总结
总结一下,汇编中一个子程序的代码长得像下面这个样子:[plain] view
plain copy
print?
push ebp ;保存ebp
mov ebp, esp ;将ebp设为当前esp值
sub esp, 4*局部变量个数 ;局部变量
push eax ;将寄存器压栈
push esi
……
mov eax, [ebp + 8] ;得到第一个参数
mov edi, [ebp + 12] ;得到第二个参数
……
pop esi ;恢复寄存器值
pop eax
mov esp, ebp ;销毁局部变量
pop ebp ;恢复ebp的值
ret 4*被压入参数个数
相关文章推荐
- android使用webview加载网页
- APNS推送证书生成与验证
- 如何用SSH密钥远程登录腾讯云linux服务器
- 程序员双屏、多屏重要性!怎么设置?
- B+/-Tree原理及mysql的索引分析
- 光标所在位置插入一串字符,并在特定位置设置光标锁定位置
- mamp装swoole扩展
- 谷歌推出最新图像识别工具Google Cloud Vision API
- DevExpress控件学习总结2(转)
- [Java] 过滤流BufferedInputStream和BufferedOutputStream
- Remove Duplicates from Sorted Array
- Ceph浅析
- POJ 2299(树状数组,离散化)
- 剑指offer系列之55:删除链表汇总重复的节点
- Android天气预报程序(一)
- mysql-python安装的各种坑
- I2C总线
- Python学习笔记——Unicode
- org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [com.XXX.util.spri
- Node.js包(JXcore)