函数栈帧
- 函数是程序中必不可少的一个部分,整个程序的运行几乎都是由一个个函数之间互相调用来完成的,我们的main函数,printf函数,还有编程时我们自己写的函数,它们时我们的程序简介高效。
那么一个函数在内存中到底是如何调用的?如何实现函数的种种功能的?
深度解析
看这样一段代码
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("ret = %d\n", ret);
return 0;
}
这段简单代码实现加法,其中Add函数实现加法,参数为两个整型变量,Add函数被main函数调用。
铺垫
1.
解析前做这样一个铺垫:
我们的main函数其实也是被其他函数调用的
以vs2008为例,代码写好后按F10开始逐过程可以看到上方有一个堆栈帧点开后如图
- main函数是被__tmainCRTStartup(void)函数调用的,而这个函数又是被mainCRTStartup(void)函数调用的。点击可进入查看源代码。
2.
内存中的地址是由寄存器来维护的,主要的寄存器有
- eax , ebx , ecx , edx 还有这次最主要用到的
ebp
(开辟空间的低地址) ,esp
(开辟空间的高地址)。 - 当在内存中为函数开辟空寂后,ebp 存这块空间的高地址, esp 存这块空间的低地址。
- 函数和局部变量在栈空间中开辟,栈孔吉娜由高地址向低地址使用。
反汇编剖析
现在我们开始研究函数的调用过程,即刚才按F10后逐过程停留在main函数,如第一幅图。
鼠标放在main函数旁边右击转到反汇编,然后在主工具栏找到-> 调试-窗口-点击监视和内存。
在监视栏找到ebp和esp的地址,同样在内存中找到以便后面观察。
- 看图体会
继续按F10逐步执行
- push为压栈向顶端压入一个地址,mov移动,sub开辟空间。
- 按F10向下执行,这几步看图解析。
- 继续连续三次压站三个寄存器
- lea寻找有效地址
rep重复,将开辟的空间全部初始化为
cc cc cc cc
这里地址统一发生了变化,因为刚才调试中断了,但效果是一样的大家注意
- 紧接着就为 a 和 b 初始化了
-在call前的两次push是将a,b形参压入栈中
- 继续向下,遇到call是按F11,call是调用call函数
- 紧接着可以看到将ebp压入了栈顶,这里的ebp是main的起始地址,为函数调用完返回main使用。
- 这后面与之前main过程类似
- 后面相加完成后将z的值放入eax中,为带回到main函数ret中
Add函数的空间就会被释放
-后面的pop是出栈到相应的寄存器中最后pop ebp就是将之前main函数的地址又返回到ebp从而使寄存器维护的区域回到main函数
随后执行printf函数最终结束main函数程序结束,函数在内存中的调用过程就是这样
作用
了解了函数在内存中的调用过程之后,一个程序在我们眼前是不是就显得很透明了呢?
那明白这些有什么作用,他的重要性又在哪里呢?
我们通过这样一段代码来体会。
void fun()
{
int tmp = 10;
int *p = (int *)(*(&tmp+1));
*(p-1) = 20;
}
int main() {
int a =0;
fun();
printf("a = %d\n",a); return 0;
}
- 试判断输出结果是什么?
- 答案是20(可以猜到)但我们要知晓他的原理
请参考倒数第三幅图理解
int *p = (int *)(*(&tmp+1));
//tmp的地址加一指向了压入的ebp(main的地址),将这个地址给指针p,p就指向了main的开始。
*(p-1) = 20;
//p的地址减1,指向了为a开辟的空间从而改变了a的值。
从而可见函数栈帧的重要性
谢谢阅读欢迎留言~