C语言函数栈帧的创建和销毁

2024-01-09 18:27:58

1.什么是函数栈帧

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


函数参数和函数返回值


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


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

我们可能常常对这些问题感到困惑:

函数是如何创建的呢?

函数又是如何调用的呢?

为什么在创建好一个变量不初始化就是随机值呢?

函数是如何将值反回来的?

参数又是如何给函数传递的?顺序又是怎样的呢??

经过这篇文章相信能让你开启一个新世界的大门;

2.函数栈帧的创建与销毁解析

在开始前先让我们认识一下我们会遇到的一些汇编指令:

寄存器:

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

汇编指令:

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

我使用的是vs2022的编译器进行的操作;

首先我们要明确一个东西,在栈上的空间是由高到低使用的;

1.栈顶指针和栈底指针

我们给函数开辟一块栈区空间,那么就是将这块空间划分给了这块函数,那么就得限制这个空间的边界,不能让别的函数在这个空间进行操作,也不能让这个函数跑到别的空间中;

当调用哪个函数的时候,esp和ebp就会跑去维护哪个函数的函数栈帧空间;

这个时候我们就可以用2个指针来维护这块区域;ebp和esp是存放的是地址;

esp用来存放栈顶的地址,ebp用来存放栈底的地址;esp~ebp之间的空间就是操作系统分配给这次函数调用所开辟的空间;

2.函数的调用关系

我们可以在编译器中查看函数的互相调用关系;


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", c);
	return 0;
}

以这段代码为例:

通过这段代码我们发现main函数也是被调用的;main函数被invoke_main()调用,这个函数又被

_scrt_common_main_seh()调用;往后依次这样的顺序;

3.函数栈帧的创建

前面我们知道了main函数也是被调用的,那么当我们执行到main函数的时候肯定也在前面创建了一个函数栈帧来调用main函数;

前面的invoke main函数肯定也有一块属于自己的函数栈帧空间;创建好了之后就开始调用main函数了,让我们具体来看看main函数是如何创建自己的函数栈帧空间的;

下面给大家上内存观察:

?push压入一个元素之后,esp的地址也变小了;

mov将esp的值给ebp,ebp指向了一个新的地址;

sub指令,esp-0E4h,0E4h(228)是一个16进制数字,地址减去数字还是一个地址,此时esp指向了一个新的地址;此时的esp-ebp的函数栈帧空间范围就已经创建好了;

执行3次push,esp的地址变小;esp指向了一块新的空间;

将edi这个地址开始向下进行9次将4个字节的空间初始化为0cccccccc;

到这个时候,main函数的函数栈的创建才正式完成;

4.局部变量的创建

内存观察:

局部变量也就全部创建好了,从这里我们也可以发现,为什么我们不初始化的话为什么里面会是随机值,这个随机值其实是在创建函数栈帧的时候自己放进去的;

5.函数调用的解析

在函数调用前我们先进行传参数;

mov 将ebp-14h这个地址向后4个字节的数据交给eax;然后将eax压栈;

mov 将ebp-8这个地址向后4个字节的数据交给ecx;然后将ecx压栈;

这一步就是传参;

接下来这一条指令非常重要,call指令将它下面一条指令的地址压入栈中保存;

jmp开始跳转到Add函数;

然后又是我们熟悉的开辟栈帧空间,创建局部变量;

我们好像貌似并没有发现Add的函数栈帧空间里面有x和y啊,那么形参在哪去了呢?我们接着往下看;

将ebp+8这个地址向后的4个字节的数据给eax;

ebp-8不就是我们之前往栈区压入的元素吗?

执行add指令,然后将ebp+0ch地址向后4个字节的元素相加到eax;

再将eax放入到ebp-8向后的4个字节的空间里;

然后开始把值往回带,我们注意最后一条mov指令,将ebp-8往后4个字节的空间的数据放入寄存器eax中;因为执行完条语句我们就会将函数栈帧销毁,所以这条代码的实际上的意义是将我们的答案存入到寄存器当中,并不是直接返回答案;

执行完后,就要回收空间还给操作系统了;

先弹出栈顶的三个元素然后将值赋给自己,然后esp+0cch,esp指向了栈底;mov将ebp的地址给esp,此时空间已经回收完毕;

弹出ebp(我们在栈上压入了一个元素,这个元素的内容是main函数的栈底地址)将ebp对值给自己,然后ebp回到了main函数的栈底的地址;

执行ret指令,恢复返回地址,别忘了我们在执行call指令的时候还将call指令单下一条指令单地址保存了起来,这个时候就放回保存的那个地址,至此又回到了main函数里面;但是此刻返回值还没有带回来;

先给esp加8,让栈顶指针回到main函数的栈顶;

mov将eax的值交给ebp-20h,那这不就是我们的c吗?至此函数的返回值也带回来了;

函数栈帧是如何销毁,形参是实参的一份临时拷贝,传参的顺序这些通过这一篇文章都能够知道

文章来源:https://blog.csdn.net/bite_free/article/details/135467746
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。