各位CSDN的uu们你们好呀,今天小雅兰来为大家介绍一个知识点——函数栈帧的创建和销毁。其实这个知识点,我们很早之前就要讲,但是因为我的一系列原因,才一直拖到了现在,那么,话不多说,让我们一起进入函数栈帧的世界吧
我们学习了前面这么多内容,不由得会想起几个问题:
- 局部变量是如何创建的?
- 为什么局部变量不初始化内容是随机的?
- 函数调用时参数是如何传递的?传参的顺序是怎样的?
- 函数调用是怎么做的?
- 函数的形参和实参分别是怎样实例化的?
- 形参和实参的关系是什么?
- 函数的返回值是如何带回的?
带着这一肚子的疑惑,就有了今天的函数栈帧的创建和销毁了。
寄存器
什么是函数栈帧
什么是栈
解析函数栈帧的创建和销毁
首先,我还得给大家拓展一个知识点——寄存器。
寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。
按照功能的不同,可将寄存器分为基本寄存器和移位寄存器两大类。
基本寄存器只能并行送入数据,也只能并行输出。
移位寄存器中的数据可以在移位脉冲作用下依次逐位右移或左移,数据既可以并行输入、并行输出,也可以串行输入、串行输出,还可以并行输入、串行输出,或串行输入、并行输出,十分灵活,用途也很广。
这边介绍一下寄存器的基本含义、基本概念、结构、工作原理、类型、存放代码满足条件、寄存器组织、寄存器寻址
相关寄存器和汇编指令
相关寄存器
- eax:通用寄存器,保留临时数据,常用于返回值
- ebx:通用寄存器,保留临时数据
- ebp:栈底寄存器
- esp:栈顶寄存器
- eip:指令寄存器,保存当前指令的下一条指令的地址
相关汇编命令
- mov:数据转移指令
- push:数据入栈,同时esp栈顶寄存器也要发生改变
- pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
- sub:减法命令
- add:加法命令
- call:函数调用,
- 1.压入返回地址
- 2.转入目标函数
- jump:通过修改eip,转入目标函数,进行调用
- ret:恢复返回地址,压入eip,类似pop eip命令
什么是函数栈帧
我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。
那函数是如何调用的?
函数的返回值又是如何带会的?
函数参数是如何传递的?
这些问题都和函数栈帧有关系。
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
函数参数和函数返回值
临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
看到这里,我们就必须还想到一个问题——什么是栈?
什么是栈
栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。 在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的。
在了解了这些准备工作之后,我们就可以进入我们的正题啦——解析函数栈帧的创建和销毁
解析函数栈帧的创建和销毁
首先我们达成一些预备知识才能有效的帮助我们理解,函数栈帧的创建和销毁。
1. 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
2. 这块空间的维护是使用了2个寄存器: esp 和 ebp , ebp 记录的是栈底的地址, esp 记录的是栈顶的地址。
3. 函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2010为例。
函数的调用堆栈
#include<stdio.h>
int Add(int x, int y)
{int z = 0;z = x + y;return z;
}
int main()
{int a = 3;int b = 5;int ret = 0;ret = Add(a, b);printf("%d\n", ret);return 0;
}
我们可以看到,main函数也确实被调用了
在VS2010中,main函数也是被其他函数调用的 __tmainCRTStartup 这个函数又是被调用的 mainCRTStartup
即,mainCRTStartup调用了__tmainCRTStartup,__tmainCRTStartup又调用了main函数
现在转到我们的反汇编,把这个显示符号名的勾勾去掉,这样方便观察
压栈(push)操作
mov操作,表示把esp的值给ebp
sub操作,表示esp的值减去0E4h
0E4h是一个十六进制数字,转为十进制为228
经过sub操作,esp的值就变了
然后,esp就指向上面开辟的某一块空间了
这一块空间,就是为我们的main函数预开辟的一块空间了,也就是main函数的栈帧
然后再是三个push操作,push了ebx、esi、edi
再是lea操作,lea表示Load Effective Address,是为加载有效地址
把[ebp+FFFFFF1Ch]的值加载到edi中,但是这个值不好观察,那我们还得把我们之前取消的显示符号名给勾上
这三个操作的意思是,把刚刚main函数的栈帧全部初始化为CCCCCCCC
dword的意思是double word(双字),一个字是两个字节,双字就是四个字节
走了这么半天,竟然还没有执行一行有效的代码!!!
#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 = 0;ret = Add(a, b);printf("%d\n", ret);return 0;
}
这就是我们的变量为什么要初始化的原因,如果不初始化的话,内存里面放的是一个随机值
接下来,就是调用函数了
这几个动作就是在传参
我们会发现,这个Add函数的指令和我们的main函数开始的指令几乎是一样的,这实际上就是在准备栈帧
其实初始化并不止这么多次,把33h这个十六进制数字换成十进制,是多少次就初始化多少次CCCCCCCC
通过画图,我们可以清楚地知道,并没有给形参创建空间,这也验证了我们之前的结论:实参传递给形参的时候,形参是实参的一份临时拷贝,改变形参是不会影响实参的
把[ebp-8]的值放到eax这个寄存器中
好啦,小雅兰今天的函数栈帧的创建和销毁的内容就到这里了,总体来说,我觉得这个内容比较地抽象,难度也是很大的,对于我们这种初学者来说,但是,不奢求一遍就把它看懂,但求每多看一遍,收获的知识点就多一点点,这样我就心满意足啦!!!