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

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

前言

函数栈帧是一个非常重要的概念,是重点也是难点,当然涉及底层方面的知识都会很难,但是对我们理解函数的创建和运用有非常重要的作用。本篇博客的目的就是了解函数栈帧的创建和销毁过程,以及帮助我们细化下面几个问题:

  • 局部变量的怎么创建?
  • 局部变量的值不初始化为什么是随机值?
  • 函数如何传参?
  • 形参与实参是怎样的关系?
  • 函数调用是怎样做的?
  • 函数调用怎样返回?

所以这次的博客能看完读懂肯定收获满满。(采用C语言和VS2013编译器讲解)

主体

在学函数栈帧之前我们需要简单介绍一下栈的概念:栈是一种数据结构,特点就是先进后出,什么意思呢?举一个例子:我们将1,2,3分别先后入栈,那么拿出栈只能先出3,再出2,最后出1。学习函数栈帧我们需要着重介绍ebp和esp这两个寄存器,这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。我们举个例子:

#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()函数时会为main()函数开辟main()函数栈帧,此时esp和ebp指针维护main()函数栈帧,当main()函数调用Add()函数时,操作系统又会为Add()函数开辟函数栈帧,此时esp和ebp指针又会维护Add()函数栈帧。
在这里插入图片描述
了解完esp和ebp指针我们进入正题,代码还是采用上面的代码,其实在编译器VS2013中,main()函数也是被调用的。在整个过程中是mainCRTStartup()函数调用_tmainCRTStartup()函数再然后__tmainCRTStartup()调用main()函数,main()函数在调用Add()函数也就是说栈空间应该是这样分配的。
在这里插入图片描述
细心的同学已经注意到了栈帧之间存在这空隙,这些空隙是用来干什么的呢?我们打开此代码的反汇编。
在这里插入图片描述
序号①~序号⑥都是为开辟main()函数栈帧做准备,上面我们讲过调用main()函数之前就开辟过了_tmainCRTStartup()函数栈帧,而序号①代码的意思是将ebp压入栈,我们知道ebp栈底指针,存放的是地址,也就是将ebp的值存入栈中(esp永远指向栈顶)。如图:
在这里插入图片描述
序号②代码的意思是将esp的值赋值给ebp,也就是说ebp和esp的值相同,指向用一个位置。如图:
在这里插入图片描述
序号③代码的意思是:esp减去0E4H(H表示16进制),函数栈帧是从高地址到低地址使用的,从图上来看就是esp指针往上移动,而这时候的esp到ebp就是为main()函数开辟的函数栈帧,如图:
在这里插入图片描述
序号④~⑥代码的意思是:将ebx、esi、edi压入栈中,esp(栈顶指针)也随之移动,如图:
在这里插入图片描述
完成了上面的操作,我们接下来再看反汇编代码:
在这里插入图片描述
红框框代码比较难以理解,简单来讲就是将为main()函数开辟的函数栈帧空间全部赋值为cccccccc,也就是初始化操作,如图:
在这里插入图片描述
当一切准备就绪,可以说也就完成了main()函数栈帧的所有准备工作,我们再看分汇编代码:
在这里插入图片描述
序号①~序号②分别代表着:在ebp-8的位置放入10(16进制0Ah),在ebp-14h的位置放入20(16进制14h),在ebp-20h的位置放入0,如图(左图:概念图,右图:内存图):
在这里插入图片描述
接下来,我们再看反汇编代码:
在这里插入图片描述

序号①代码的意思是将ebp-14h地址的值放到eax中去,我们回头来看ebp-14h中存放的就是b的值(20),也就是将20赋值给eax;序号②代码的意思是将eax压入栈中,同理序号③和序号④也是将(10)放入ecx中,再将ecx压入栈中,这就是实参的传参和形参的创建。如图所示:
在这里插入图片描述
序号⑤中call指令是将下一条指令的地址(00C21450)压入栈中,如图所示:(至于为什么这么做,我们接着往下看)
在这里插入图片描述
接下来就是就是调用add()函数
在这里插入图片描述
红框框中的代码就是为Add()函数开辟栈帧,我们就不再赘述。如图:
在这里插入图片描述
序号①代码的意思就是将0放入ebp-8(z)的位置,序号②代码的意思就是将ebp+8中的内容(10),也就是形参x,和ebp+0Ch(ebp+12)中的内容(20),也就是形参y,相加得到30再放到eax中去,最后将eax的值赋值给ebp-8的位置(也就是z)。如图所示:
在这里插入图片描述
序号③中的代码意思是将ebp-8(z)中的值存放再eax寄存器中(为了返回z的值),这时候我们再看反汇编:
在这里插入图片描述
红框框中表示依次弹出edi,esi,ebx,代码①表示将ebp的值赋值给esp,也就是将Add()内容释放掉,如图所示:
在这里插入图片描述
序号②代码表示弹出栈顶元素,并存放到ebp中,我们知道此时的栈顶存的是main()函数栈帧底的地址,因此此时ebp就可以回到main()函数的栈底,如图所示:
在这里插入图片描述
序号③中ret指令表示的是return并弹出栈顶,也就是返回到原来main()函数执行的位置,而因为调用过Add()函数,编译器并不知道main()函数原来的执行到哪呢,此时的ret指令就是解决这个问题,此时栈顶就是下一条指令的地址(00C21450),这也回答了我们上面设下的悬念。如图所示:
在这里插入图片描述
这时我们再看反汇编代码:
在这里插入图片描述
序号①代码表示:esp的地址加上8,也就是说,esp往下移2个整型,即销毁形参x和y。序号②则表示将eax中的值(z传回来的30)传到ebp-20h中(c)如图所示:
在这里插入图片描述
红框框则表示调用了printf()函数,当然也需要栈帧创建销毁返回等过程,最后就是main()函数的销毁,原理与Add()函数的销毁一样,这里也就不再赘述。那么有关函数栈帧的创建和销毁就全部讲解完了。
现在我们再回头看那几个问题:

  • 局部变量的怎么创建?
  • 局部变量的值不初始化为什么是随机值?
  • 函数如何传参?
  • 形参与实参是怎样的关系?
  • 函数调用是怎样做的?
  • 函数调用怎样返回?

这几个问题是不是都能够回答了。
局部变量的创建:首先我们为这个函数分配好栈帧空间,初始化一部分空间之后,在这栈帧空间给局部变量分配一定的空间。
局部变量不初始化是随机值:因为随机值是编译器放进去的(VS2013是cccccccc),如果初始化,就会把随机值覆盖掉。
函数如何传参:在没有调用函数的时候,就已经将两个参数放入栈中了,并且是从右向左压栈,并且可以通过esp指针的偏移量找回形参。
形参和实参的关系:形参是在压栈时开辟的空间,它和实参只是值是相同的,空间是独立的,形参是实参的一份临时拷贝,形参不会影响实参(只是数值上不会影响)。
函数调用与函数返回:上面也已经详细介绍了,主要要注意函数的返回,特别是call指令的作用。
如果看到这里,先给自己鼓个掌吧,这部分处于底层,非常的抽象,因此晦涩难懂,当然如果搞懂带来的好处也是非常大的,如果有什么问题,或者建议也非常希望留言告诉我,谢谢大家了!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Solitudefire

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值