前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C语言底层】函数栈帧的创建和销毁

【C语言底层】函数栈帧的创建和销毁

作者头像
s-little-monster
发布2024-06-06 20:39:45
840
发布2024-06-06 20:39:45
举报
文章被收录于专栏:乐意学点小编程

函数栈帧的创建和销毁在所有编译器中都是大同小异的,不同的编译器会有不同的方式,但是了解到了简单的底层的这些方法后,其他的编译器都是在此基础上修饰,不必深究。

1、寄存器 ebp,esp 这两个寄存器中存放的是地址,用来维护函数栈帧 2、编译器的选择 最好使用visual 6.0来观察,它更加简洁,我们用到的是vs2013,因为越早的编译器观察到的过程越不复杂。

我们来给到一个简单的加法函数

代码语言:javascript
复制
#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",c);
	return 0;
}

最粗略的整体的逻辑

我们知道每一个函数调用都要在栈区创建一块空间,一般是由高地址向低地址使用,main函数的使用也要开辟栈帧

esp存入函数低位置的地址,叫做栈顶指针,ebp存入函数高位置的地址,叫做栈底指针。 我们用调试的方法来观察过程。

main函数也是被调用的,被 __tmainCRTStartup() 这个函数调用,而这个函数又被下面的 mainCRTStartup() 调用,这样也就是能够解释为什么我们的main方法要return 0 了,它返回到了调用它的函数 __tmainCRTStartup()里面

当然在一开始的时候我们也会为这两个函数创建空间,在main函数之前

调用Add函数时再创建空间

汇编语言的指令 打开反汇编,我们可以看到汇编语言对程序的操作,这里push叫压栈,push ebp就是将一个叫做ebp的量压到栈顶上边(这里涉及到监视窗口可以监视到ebp确实是地址小于的正好在 __tmainCRTStartup() 函数的上方,有兴趣的大家可以打开监视窗口查看一下,这里我们为了缩短篇幅只讲结果)(与push相对的叫做pop,出栈,从栈顶删除一个元素)

在我们创建 __tmainCRTStartup() 这个函数时,接着push ebp 如下两图所示 (因为esp维护的是栈顶,所以push之后esp要跟着变化)

然后mov ebp,esp 是把esp的值给ebp,此时两个值相等,同时指向上图esp所指的地方。

第三条指令sub是减法,就是让esp-0E4h,改变栈顶的地址,esp指向了上边的某一区域(这里不会越出界限)

紫色即main函数的栈帧

然后把ebx,esi,edi 三个push上去

lea 即 load effective address 加载有效地址 第四条指令即将后边的ebp+FFFFFF1Ch加载到edi里边去,即ebp-0E4h 然后将39h和0CCCCCCCCh这个数字分别给到ecx和eax。(这里不需要管这两个数字是什么)

word是两个两个字节,dword 就是 double word 四个字节 然后将这从edi开始向下39h个数字全部变成0CCCCCCCCh,即下图区域

以上就是为了正式的代码做铺垫

然后就是赋值a赋值b 然后进入到Add函数中

传参过程 然后mov push 给到eax和ecx

call是调用函数,它会压栈一个00C21450,这是call指令的下一条指令,以便call返回时继续使用

这里的汇编语言指令在前面都说到过,我们跳过继续说

注意这里先传b再传a,传参的顺序是从右往左的,在汇编指令中我们可以很明显的发现,传参的方式,就是调用实参出来给到形参,而不是形参的单独创建,这有利于我们了解形参和实参的关系。 形参是实参的一份临时拷贝(中肯的一针见血的) 这里我们会有一个疑问,为什么return z 之后这个函数不是销毁了吗?那值不是也会随之销毁吗? 其实这里看到return z 后面的这个汇编指令,它把z的值(z的地址就是ebp-8)赋值给了eax,销毁之后再把eax的值传出去 三个pop将他们逐出去了

它们就被回收了 然后把ebp赋值给ebp,此时两个指针指向相同的地方,上面的函数所占的空间被系统回收了,即Add函数被回收了

此时再pop最上面的ebp,这里面存放的是最开始main函数的栈底指针,这样我们就能很容易的找到main函数栈底的位置,此时ebp指针回到这个位置,然后esp指向00C21450的这个位置,并且此时两指针之间的这一区域就是main函数所占的区域。 然后ret返回到call指令的下一条指令,即00C21450,然后将此地址也弹出,指针指向下一位(标黄)

然后下一步将形参x,y所占的空间释放esp+8,往下走,,然后就把eax的值给到ebp-20h了,也就是z的值给了c:z在销毁前把值传给eax,eax在00C21453这一步时将值传给ebp-20h,在这个位置的值就是c。

到现在,我把函数栈帧的创建和销毁的过程大致梳理了一遍,我在学完之后有一种恍然大悟的感觉,希望这篇能够帮到大家。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-03-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档