您的位置:首页 > 其它

浅谈函数的调用过程,栈帧的创建以及销毁

2017-12-10 21:49 405 查看
我们知道每一次函数调用都是都是一个过程,这个过程我们称之为: 函数的调用。
而main函数是一个程序的入口,那么在运行程序的时候main函数是第一个被调用的函数吗?
下面我们来看一段代码:


#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;
}


打开调用堆栈



我们发现main函数是被__tmainCRTStartup函数调用,而__tmainCRTStartup又被mainCRTStartup调用。

函数的每一个调用都是一个过程,我们称之为: 函数的调用过程

这个过程就要为函数开辟空间,用于函数调用中的临时变量保存,现场保护。而开辟出来的空间我们称之为:函数栈帧

————————————————————————————————

在介绍栈帧之前我们有必要介绍一下程序的地址空间,如下图:



程序的地址空间从低地址到高地址分为以下几个部分:

程序代码区:存放函数体(类成员函数和全局函数)的二进制代码。

文字常量区:常量字符串放在这里,程序结束后由系统释放。

全局区:又称为静态区,存放全局变量,静态数据,常量。程序结束后由系统释放。

堆区:由程序员分配释放,若程序员不释放,程序结束后由OS回收,分配方式类似于链表。

栈区:由编译器自动分配释放,存放为运行函数而分配的局部变量,函数参数,返回数据,返回地址等。其操作方式类似于数据结构中的栈。

另外,栈区和堆区相向而生

下面来介绍一下栈帧,在函数的调用过程中栈帧形成于程序的栈区。

如下图所示:



要想了解栈帧,我们必须了解两个寄存器ebp和esp。在函数的调用过程中,ebp和esp分别存放了维护这个栈的的栈底和栈顶的指针。

首先我们通过汇编来研究栈帧的形成过程:



在程序的执行过程中,main函数形成栈帧,ebp存储栈底指针,esp存储栈顶指针,而esp与ebp指针所指向的地址之间存储着运行函数而分配的局部变量以及函数参数。存储的顺序是先a后b,并且是从高地址到低地址存储。



接下来,main函数调用Add函数:
首先,将Add函数中的形参实例化,实例化的顺序是从右到左,也就是先实例化 b ,再实例化 a ,esp存储的指针也向低地址处移动。形参实例化的具体操作如下图:




先将实参 b 存到寄存器eax中,然后将eax压入栈中;然后将实参 a 存到寄存器ecx中,再将ecx压入栈中。

实例化完成之后汇编代码执行call指令,call指令有如下两个作用:



1、将当前正在执行的指令的下一条地址压入栈中。(main:ret)
2、跳转至指定函数。(jmp实现)


接下来跳转至Add函数:



在Add函数中首先将栈底指针压入栈中;然后将esp的地址送到ebp,这时的esp与ebp所存的指针指向同一点。

接下来,将esp中地址的值减44h,这相当于为Add函数开辟了新的栈帧。


再向下执行:



执行 int a = x + y;

将a的值放入eax中,然后与b相加,再把相加的值赋给z。

执行 return z;

将z的值放入eax中。

再往下,将ebp的值赋给esp,此时esp与ebp再次指向同一位置。

然后,执行pop指令,此时的pop指令同样有两个作用:

1、将栈顶(esp)上移。

2、将pop出的值放到ebp中。

所以执行完pop指令后,ebp返回main栈帧的栈底。

再往下,执行ret,ret的作用也有两个:

1、将栈顶的值弹出。

2、将弹出的值放入EIP。

至此函数的调用过程就结束了,接下来:



将esp的值加8,也就是将栈顶指针指向main函数栈帧的栈顶。

然后将eax中存放的值放入main函数的栈帧中。

函数的调用过程,栈帧的创建以及销毁大体就是这样一个过程,我的解释中还有许多不到位的地方,欢迎大家批评指正!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: