文章目录
- 一、什么是函数栈帧?
- 二、理解函数栈帧能解决什么问题?
- 三、函数栈帧的创建和销毁解析
- 3.1什么是栈?
- 3.2认识相关寄存器和相关指令
- 3.3解析函数栈帧的创建和销毁
- 3.3.1`main`函数栈帧的创建准备
- 3.3.2创建`main`函数栈帧:
- 3.3.3`main`函数栈帧中创建局部变量
- 3.3.4传参
- 3.3.5创建`Add`函数栈帧
- 3.3.6`Add`函数栈帧中创建局部变量
- 3.3.7返回`z`
- 3.3.8销毁`Add`函数栈帧
- 3.3.9`main`获得`Add`函数返回的值
一、什么是函数栈帧?
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
- 函数参数和函数返回值
- 临时变量(包括函数的非静态的局部变量以及编译器生产的其它临时变量)
- 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)
二、理解函数栈帧能解决什么问题?
- 局部变量是如何创建的?
- 为什么局部变量不初始化内容是随机的?
- 函数调用时参数是如何传递的?传参的顺序是怎样的?
- 函数的形参和实参是如何示例化的?
- 函数的返回值是如何带回的?
三、函数栈帧的创建和销毁解析
3.1什么是栈?
在经典的计算机科学中,栈是一种特殊的容器,遵循先入先出(First in First out, FIFO)。
在计算机系统中,栈则是一个具有FIFO属性的动态内存区域。程序可以将数据压入栈中,使得栈增大;也可以将数据从栈顶弹出,而弹出操作使得栈减小。栈总是向下增长(由高地址向低地址的)。
3.2认识相关寄存器和相关指令
相关寄存器eax:通用寄存器,保留临时数据,常用于返回值ebx:通用寄存器,保留临时数据eip:指令寄存器,保存当前指令的下一条地址esp:栈顶寄存器ebp:栈底寄存器
3.3解析函数栈帧的创建和销毁
声明:
下面的图中画的格子数并不代表编译器具体分配的空间大小,只是为了方便理解,我随意画的
每一次函数调用,都要为本次函数调用开辟一块空间,就是函数栈帧的空间。开辟的空间遵循FIFO。
每一个函数开辟的空间是使用寄存器esp和ebp维护的,ebp记录栈底的地址,esp记录栈顶的地址。
以下面的代码为例,来介绍函数栈帧的创建和销毁:
#include<stdio.h>intAdd(intx,inty){intz=0;z=x+y;returnz;}intmain(){inta=3;intb=5;intret=0;ret=Add(a,b);printf("%d\n",ret);return0;}3.3.1main函数栈帧的创建准备
我们都知道程序都是从main开始运行的,但是main也是被别的函数调用的。mainCRTStartup函数调用_tmainCRTStartup函数,然后_tmainCRTStartup函数调用main函数。
3.3.2创建main函数栈帧:
首先将ebp压入_tmainCRTStartup函数栈帧中,然后esp的值减小(栈中每压入一个元素,栈都要增大,栈顶改变,指向栈顶的指针就要改变,栈顶是低地址,栈底是高地址),接着将esp的值赋值给ebp,
再然后esp的值减小(减多少具体看实际要用到的空间),为main函数预分配空间
依次将ebx、esi、edi压栈
上面三个寄存器在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复
将事先为main栈帧预开辟的空间填充为0xcccccccc
3.3.3main函数栈帧中创建局部变量
每个局部变量之间间隔多大空间是根据编译器定的
变量如果不初始化,对应的值就是0xcccccccc(对应的字符是烫),这就是为什么屏幕会打印烫烫烫烫烫烫。
3.3.4传参
将a和b的值压栈,将操作指令的下一条指令的地址压入栈中,方便后续返回main函数中继续操作
3.3.5创建Add函数栈帧
将main栈底地址压入main函数栈帧中,接着将esp的值给ebp,然后esp的值减小,为Add函数调用分配内存,最后依次将ebx、esi、edi压入栈中。
同main函数一样,将分配的栈帧空间填充为0xcccccccc
3.3.6Add函数栈帧中创建局部变量
创建局部变量z
计算a + b的值,赋值给z:将a的形参加到eax中,再将b的形参也加到eax中,此时eax的值就是8,最后将eax的值赋值给z
所以说函数的形参是实参的一份拷贝
3.3.7返回z
将z的值给了eax,在返回到main中调用eax就可以了
解决了函数结束之后,局部变量z也随之销毁,导致main函数无法得到返回值的问题
3.3.8销毁Add函数栈帧
依次弹出edi、esi、ebx,将此时ebp的值给了esp,ebp的值变为main函数栈帧栈底的地址(在创建Add函数栈帧前已经存过),此时ebp和esp分别指向main函数栈帧的栈底和栈顶,Add函数栈帧已经销毁
3.3.9main获得Add函数返回的值
在返回main后,该如何继续执行操作呢?
这时就会用到之前存放操作指令的下一条指令的地址,根据这个地址的指令执行后面的操作,弹出a和b的形参
将eax(之前将Add函数返回值暂存与eax中)的值给了变量ret
以上仅仅是我个人的粗浅理解,只描述了大概的过程,很多细节都没有涉及。我希望通过我的文章,您对二、理解函数栈帧能解决什么问题?有了大概的理解。同时,也欢迎各位大佬的点评。