函数栈帧的创建与销毁

函数栈帧的创建与销毁

  • 一. 什么是函数栈帧
  • 二. 函数栈帧的作用
  • 三. 函数栈帧的创建和销毁的流程
    • 1. 为 main 函数开辟栈帧
    • 2. 执行 main 函数中的代码
    • 3. add 函数开辟栈帧前的准备
    • 4. 为 add 函数创建栈帧
    • 5. 执行 add 函数中的代码
    • 6. add 函数栈帧的销毁
    • 7. 继续main 函数中代码的执行
    • 7. main 函数栈帧的销毁
  • 四. 几个相关的问题
    • 1.局部变量是怎么创建的?
    • 2.为什么局部变量不初始化的值是随机值?
    • 3.函数是怎么传参的?传参的顺序是怎么样的?
    • 4.形参和实参是什么关系?
    • 5.函数调用是怎么做的?
    • 6.函数调用结束后怎么返回的?

上面以下面这段代码为例的一段动画, 后面都是以这段代码(动画)为例进行的讲解

#include 

int add(int x, int y) 
{
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 10;
	int b = 20;
	int c = 0;
	
	c = add(a, b);

	printf("%d\n", c);
	return 0;
}

一. 什么是函数栈帧

  1. 函数栈帧是程序运行过程中,每次函数调用时所开辟的一块空间
  2. 用于存储当前函数的局部变量、参数、返回地址和一些有关函数执行的其他信息。

二. 函数栈帧的作用

通过寄存器 ebp (栈底指针)和 esp (栈顶指针)维护当前函数的函数栈帧。
保证在程序运行过程中能正确地管理数据和控制函数调用的流程。

三. 函数栈帧的创建和销毁的流程

1. 为 main 函数开辟栈帧

  1. main 函数也是被其他的函数调用的,(这个可以通过观察编译器的函数调用堆栈看到),
    所以也需要做函数栈帧开辟前的准备工作, 但是这个过程与main 函数调用 add 函数开辟栈帧前的准备工作是一致的, 所以就只介绍了 add 函数开辟栈帧前的准备 。
  2. 开辟 main 函数栈帧
  3. 寄存器 ebx, esi, edi 先后入栈
    (入栈的不是寄存器,是这三个寄存器里面存的数值), 这三个寄存器入栈是为了将他们之前的数据保存起来, 之后会用到这三个寄存器,会将里面的值改了,为了能还原回来,就先记住他们之前的值
  4. 通过使用前面的三个寄存器,将 main 函数栈帧那块空间全部初始化为 0x CC CC CC CC
    (这就解释了为什么我们局部变量不初始化时是一个随机值, 0x CC CC 被当作文本就是"烫")

2. 执行 main 函数中的代码

  1. 先后为变量 a, b , c 开辟空间并初始化为对应的值

3. add 函数开辟栈帧前的准备

  1. 根据传入的参数, 从右到左依次入栈
    即先将 b 复制一份入栈, 再将 a 复制一份入栈分别记为 b’, a’
    (这就是为什么我们更改形参不会影响实际的变量)
    (后面在 add 函数中并没有再给 变量 x, y 开辟空间, 而是通过找到 变量 a’, b’, 直接进行运算)
  2. 记录 main 函数中调用过 add 函数后的下一条指令的地址
    本例就是对变量 c 进行赋值的地址, 因为调用过 add 函数后, 要将结果赋值给 c, 目的就是调用过函数之后还能回到之前的位置继续往下执行
  3. main 函数栈帧栈底的位置入栈(也就是当前 ebp 的值)
    因为 ebp, esp 维护的是当前函数的栈帧, 所以调用过 add 函数后, 就要维护 add 函数栈帧, 调用过之后还得回来继续维护 main 函数栈帧, 所以得记得 之前 main 函数的栈底, 为什么不用记录栈顶呢? 因为 esp 会根据栈的变化一直记录的都是栈顶

4. 为 add 函数创建栈帧

准备工作已经做完了, 开始正式开辟栈帧, 这里与 开辟 main 函数栈帧一样

  1. 寄存器 ebx, esi, edi 先后入栈(入栈的不是寄存器,是这三个寄存器里面存的数值), 这三个寄存器入栈是为了将他们之前的数据保存起来, 之后会用到这三个寄存器,会将里面的值改了,为了能还原回来,就先记住他们之前的值
  2. 通过使用前面的三个寄存器,将 main 函数栈帧那块空间全部初始化为 0x CC CC CC CC(这就解释了为什么我们局部变量不初始化时是一个随机值)

5. 执行 add 函数中的代码

  1. 为 变量 z 开辟空间并初始化为对应的值 0
  2. 通过栈底指针 ebp + 偏移量找到形参a’, b’ 并直接进行运算得到 结果, 并更新 z 值
    (所以 add 函数中并没有再给 变量 x, y 开辟空间, 而是通过找到 变量 a’, b’, 直接进行运算)

6. add 函数栈帧的销毁

  1. 先将要返回的结果 z 放到 eax 寄存器中,因为函数调用结束后对应的栈帧都会被销毁
  2. edi, esi, ebx 以及 add 函数栈帧依次出栈销毁
  3. main 函数栈底地址出栈, 这样 ebp 栈底指针就能回到之前的 main 函数栈底, 继续维护 main 函数栈帧
  4. 调用 add 函数后的下一条地址出栈, 这样才能回到 main 函数继续往下执行
  5. 形参a‘, b’ 出栈

7. 继续main 函数中代码的执行

  1. 寄存器 eax 存储的返回值 赋值给 c
  2. 记录往下执行剩余的代码

7. main 函数栈帧的销毁

与 add 函数栈帧的销毁流程一致

  1. 用寄存器记录 返回值(这里没有返回值)
  2. edi, esi, ebx 以及 main 函数栈帧依次出栈销毁
  3. _tmainCRTStartup (调用 main 函数的那个函数) 函数栈底地址出栈
    这样 ebp 栈底指针就能回到之前的 _tmainCRTStartup 函数栈底, 继续维护 _tmainCRTStartup 函数栈帧 (这里没画出来)
  4. 下一条地址出栈, 这样才能回到 _tmainCRTStartup 函数继续往下执行 (这里没画出来)
  5. 传给 main 函数的形参出栈(这里没有参数)

四. 几个相关的问题

1.局部变量是怎么创建的?

  1. 先为函数创建栈帧空间
  2. 并给这段空间初始化为 0 x CC CC CC CC 后
  3. 再为局部变量分配空间并初始化

2.为什么局部变量不初始化的值是随机值?

因为这个随机值是编译器放的, 如果我们初始化了, 那么就将这个随机值覆盖

3.函数是怎么传参的?传参的顺序是怎么样的?

  1. 在函数栈帧开辟前就从右往左将参数依次压栈
  2. 并且在函数栈帧中, 并没有再为形参开辟空间, 而是通过指针偏移量找到压入栈的形参

4.形参和实参是什么关系?

  1. 形参是实参的一份临时拷贝, 是在函数调用时压栈开辟空间, 值与实参相同,
  2. 所以改变形参不会影响实参

5.函数调用是怎么做的?

上述就是详细流程
包括 准备工作, 创建栈帧, 执行代码, 存储返回值, 销毁栈帧, 回到原来的栈帧

6.函数调用结束后怎么返回的?

  1. 函数调用之前就将下一条指令的地址入栈, 然后将本函数栈帧的栈底地址入栈, 所以函数调用结束后, 出栈就能找到之前栈底位置, 从而继续维护之前的函数栈帧, 再出栈就找到下一条指令的地址, 从而能够继续往下执行
  2. 返回值是通过寄存器带回来的

好啦, 以上就是我对函数栈帧开辟和销毁的理解, 希望能帮到你, 评论区欢迎指正!

你可能感兴趣的:(c语言)