函数栈帧的创建与销毁

前言

函数栈帧的创建与销毁在不同编译器下,函数调用过程中栈帧的创建略有差异,具体细节取决于编译器的实现,但大体逻辑是一致的。(在使用编译器时,建议不要使用太高级的编译器,编译器越高级,越智能,越不容易观察(函数栈帧的过程封装的越不好去看)。)

认识相关寄存器

esp:栈顶指针
ebp:栈底指针

eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址

注:

  • esp,ebp这两个寄存器存放的是地址,这两个地址是用来维护函数栈帧的。
  • 每一个函数调用都要在栈区创建一个空间
  • 在调用哪个函数,esp和ebp寄存器维护的就是哪个函数的函数栈帧,这两个寄存器之间的空间就是为这次函数调用所分配的空间(函数栈帧)
  • 栈区的使用习惯是先使用高地址再使用低地址

认识相关汇编命令

mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令

详解思路图

代码:

#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 c = 0;

	c = Add(a, b);

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

main函数也是要被调用的

在VS2019中main函数是被其他函数调用的,以下是调用main函数的函数
在这里插入图片描述

在这里插入图片描述

在VS2013中main函数是被_tmainCRTStartup这个函数调用的,而_tmainCRTStartup函数是被mainCRTStartup函数调用的。

调用main函数的函数栈帧以及main函数栈帧的开辟:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

补充:

  • 压栈(push)是给栈顶放一个元素
  • 出栈(pop)是从栈顶拿出一元素

main函数中有效的代码:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

注:以下图中因为是函数栈帧的销毁如果删除部分信息导致不易查看,函数栈帧的划分查看以上图(因为是函数栈帧销毁的过程栈帧范围不易表明)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

main函数栈帧的销毁:

在这里插入图片描述

总结:

  • 局部变量是怎么创建的?
    局部变量的创建首先为这个函数分配好栈帧空间,栈帧空间里面初始化好一部分空间之后,然后给局部变量在栈帧空间里分配一点空间
  • 为什么局部变量的值是随机值?
    因为随机值是放进去的,局部变量如果不初始化,栈帧空间里面的值是随机放进去的。这时如果局部变量初始化,这时就把随机值覆盖了。
  • 函数是怎么传参的?传参的顺序是怎样的?
    当调用函数时其实还没有调用时已经把要传递的参数从右向左开始压栈压进去了,当进入函数时在函数栈帧里通过指针偏移量可以找到形参。
  • 形参和实参是什么关系?
    形参确实是压栈的时候开辟的空间,它和实参只是值上是相同的,空间是独立的,所以形参是实参的一份临时拷贝。改变形参不会影响实参。
  • 函数调用是怎么做的?
  1. 将要传递的参数从右向左赋值给相应的寄存器中,然后从右向左进行相应的压栈。
  2. 将call指令的下一条指令的地址压栈(目的是函数调用完之后,能够回到call指令的下一条指令中去)
  3. 进入相应的函数栈帧时,把调用方所在函数栈帧的ebp压栈,创建函数栈帧
  4. 当在函数栈帧中用到形参时,会根据相应的寄存器(ebp)的偏移量找到参数的值
  5. 函数结束时,会将返回值(局部变量的值)通过寄存器(eax)进行保存
  6. 函数结束时,销毁函数栈帧,将ebp赋值给esp,再将栈顶的数据(之前存储调用方函数栈帧ebp的值)出栈赋给ebp同时esp也指到了相应的位置。此时esp和ebp寄存器开始维护调用方函数的栈帧空间
  7. 通过ret指令(ret指令的作用是从栈顶弹出了call指令的下一条指令地址然后跳到那去)回到call指令的下一条指令地址中,继续执行调用方所在函数中没有执行的语句。
  8. 将调用该函数的返回值(通过寄存器eax进行赋值)赋值给调用方
  9. 函数调用结束
  • 函数调用是结束后怎么返回的?
    调用之前就把call指令的下一条指令的地址压栈了,把ebp调用这个函数的上一个函数的栈帧的ebp存进去了,当函数调用完返回时出栈ebp就能够找到原始(上一个函数)调用的ebp,然后指针往下走的时候就能够找到esp的地址—回到栈帧空间。记住call指令的下一条指令的地址,当往回返的时候就可以跳转到call指令的下一条指令的地址,让函数调用的时候可以返回,返回值是通过寄存器的方式带回来的。

注:

  1. 调用函数,需要先形成临时拷贝,形成过程是从右向左的
  2. 临时空间的开辟,是在对应函数栈帧内部开辟的
  3. 函数调用完毕,栈帧结构被释放掉
  4. 临时变量具有临时性的本质:栈帧具有临时性
  5. 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
  6. 函数调用,因拷贝所形成的临时变量,变量和变量之间的位置关系是有规律的
  • 寄存器是集成到CPU上的
  • 函数的地址不是ebp地址。
  • 编译器自己会主动计算要多大空间(不会出现函数预开辟空间不够用这一现象),每个函数预开辟空间的大小是不一样的,这时编译器做的,开辟多大空间是不确定的。
  • 函数的形参是放在调用方的函数栈帧中的,这些空间增长开辟都是在调用方的函数栈帧中的完成的。

猜你喜欢

转载自blog.csdn.net/AI_ELF/article/details/119509328