0
点赞
收藏
分享

微信扫一扫

一篇文章带你充分了解函数栈帧

_鱼与渔_ 2022-04-24 阅读 92
c语言

目录

 

1.什么是函数栈帧?

2.什么是栈?

3.相关寄存器和汇编指令

3.1相关寄存器

3.2相关汇编命令

4.函数栈帧的创建和销毁

 4.1函数栈帧的创建

 4.2函数栈帧的销毁


 

1.什么是函数栈帧?

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

函数栈帧就是函数调用过程中在程序的调用栈,所开辟的空间,这些空间是用来存放:

函数参数和函数返回值;

临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量);

保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

2.什么是栈?

栈,是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。

在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。在我们常见的i386或者x86-64下,栈顶由成为esp的寄存器进行定位的。

3.相关寄存器和汇编指令

3.1相关寄存器

eax:通用寄存器,保留临时数据,常用于返回值。

ebx:通用寄存器,保留临时数

ebp:栈底寄存器。

esp:栈顶寄存器。

eip:指令寄存器,保存当前指令的下一条指令的。

3.2相关汇编命令

mov:数据转移指令。

push:数据入栈,同时esp栈顶寄存器也要发生改变。

pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变。

sub:减法命令。

add:加法命令。

call:函数调用,1. 压入返回地址2.转入目标函数。

jump:通过修改eip,转入目标函数,进行调用。

ret:恢复返回地址,压入eip,类似popeip命令。

4.函数栈帧的创建和销毁

首先我们要知道,每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。

这块空间的维护是使用了2个寄存器:esp和ebp,ebp记录的是栈底的地址,esp记录的是栈顶的地址。

如下图所示:

 

函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2019为例。

演示代码:

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

进行调试:调试进入Add函数后,我们就可以观察到函数的调用堆栈。

 函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到,main函数调用之前,是由invoke_main函数来调用main函数。

那我们可以确定,invoke_main函数应该会有自己的栈帧,main函数和Add函数也会维护自己的栈帧,每个函数栈帧都有自己的ebp和esp来维护栈帧空间。

那接下来我们从main函数的栈帧创建开始讲解:

调试到main函数开始执行的第一行,右击鼠标转到反汇编。

 4.1函数栈帧的创建

我们接下来一行行解析汇编代码

00BE1820pushebp //把ebp寄存器中的值进行压栈,此时的ebp中存放的是invoke_main函数栈帧的ebp,esp-4

00BE1821movebp,esp //move指令会把esp的值存放到ebp中,相当于产生了main函数的

ebp,这个值就是invoke_main函数栈帧的esp


 接下来我们再分析main函数中的核心代码:

 

Add函数的传参

 函数调用过程

 call指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

当我们跳转到Add函数,就要开始观察Add函数的反汇编代码了。

 代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。

在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。

1. 将main函数的ebp压栈

2. 计算新的ebp和esp

3. 将ebx,esi,edi寄存器的值保存

4. 计算求和,在计算求和的时候,我们是通过ebp中的地址进行偏移访问到了函数调用前压栈进去的参数,这就是形参访问。

5. 将求出的和放在eax寄存器准备带回

图片中的a'和b'其实就是Add函数的形参x,y。这里的分析很好的说明了函数的传参过程,以及函数在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。

 4.2函数栈帧的销毁

当函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。

那具体是怎么销毁的呢?我们看一下反汇编代码。

回到了call指令的下一条指令的地方: 

但调用完Add函数,回到main函数的时候,继续往下执行,可以看到:

 

到这里就给大家完整的演示了main函数栈帧的创建,Add函数栈帧的创建和销毁的过程 。

举报

相关推荐

0 条评论