函数栈帧的创建和销毁

目录

前期学习的困惑

寄存器

具体场景

函数栈帧的创建

函数栈帧的销毁


前期学习的困惑

在我们前期学习的时候,可能会有一些困惑,比如:

1、局部变量究竟是怎么创建的?

2、为什么局部变量的值是随机值?

3、函数是怎么传参的,传的顺序又是怎样的?

4、形参和实参究竟是什么关系?

5、函数是怎样调用的,调用后怎么返回?

我们可能只是知道要这么做,却对其原理一知半解,学了今天的内容,你将会有更深层次的认识。

寄存器

要了解函数栈帧,首先得了解寄存器。

寄存器是CPU内部用来存储数据的小型区域,其存储空间小但是运行速度非常快,一般频繁大量使用的数据都被存放在寄存器中。

寄存器有eax,ebx,ecx,edx ,还有ebp,esp。今天这几个寄存器都会见到,重点是ebp和esp。

ebp,esp这两个寄存器中存的是地址,这两个地址是用来维护函数栈帧的。

每一个函数调用,都需要在栈区创建一个空间。

具体场景

以Add函数为例。

int Add(int x, int y)
{
	int z = x + y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	printf("%d\n", c);
	return 0;
}

写一个简单的Add函数,分得足够细,方便我们观察。

在调用main函数时,我们需要在栈区上为它开辟一块空间。称为main函数的栈帧空间。

PS:是当前的main函数

 那么这个空间怎么维护呢?是依靠上面说的ebp和esp寄存器来维护的。

注意:调用哪个函数,ebp和esp就跑去维护哪个函数的栈帧。ebp和esp之间的空间就是为当前调用函数所分配的空间。

通常称ebp为栈底指针,esp为栈顶指针。  其实,栈区上是先使用高地址再使用低地址,由高地址向低地址消耗空间,向栈区里放数据时,也是从顶部开始放数据。ebp维护的是栈底,esp维护的是栈顶。

我们来看一下main函数调用堆栈。

 可以看到,其实main函数也是被调用的,被__tmainCRTStartup调用,而它又是被mainCRTStartup调用的。

函数栈帧的创建

下面我们来看main函数的细节。

 F10调试起来,右击转到反汇编,我们来看详细细节。

 这就是我们的汇编代码,或许你看不太明白也没接触过,但没关系,跟着我的思路走,也能看懂。

首先,第一行,push操作,意思是“压栈”,将ebp压到栈顶上。

PS:push(压栈):从栈顶放入一个元素;

        pop(出栈):从栈顶删除一个元素。

 大概就是这么一个过程。原来ebp指向栈底,现在将它压到栈顶去,同时,esp也就向上移动,再次指向栈顶。

然后第二行:move操作,意思是:将esp里的值放到ebp里面去。

第三行:sub操作,意思是:esp减去0E4H大小的地址。

接下来,进行了三次push操作,注意:push是把东西压到栈顶,push完一次后,esp也要随之指向新的栈顶。

接下来是个关键点!

 首先,执行lea操作,“加载有效地址”,将ebp-0E4h的地址放到edi中去; 然后,下面三行的意思是:将从edi开始的39h这么大的空间全部初始化为0ccccccch。

 可以看到效果,为main函数开辟的空间里所有内容都被初始化成了0cccccccch。

其实到这里还没有执行一条C语言有关代码,全都是在为main函数开辟空间。

下面到int a才开始执行C语言相关内容。

接下来就是初始化a,b,c了。我们来看看是怎么往栈里面存入值的。

第一句:将ebp-8的位置放入0Ah,0Ah在十进制中也就是10;同理,第二句、第三句也一样,往ebp-14h的位置放14h,往ebp-20h的位置放0 。

 所以试想一下,如果我们写代码时没有初始化,那么里面存的东西就是ccccccc了。

接着往下。

 这一步其实就是传参,但是因为不熟悉汇编语言,大家可能没感觉。这里是将ebp-14h(也就是b)放入eax, 将ebp-8(也就是a)放入ecx 。再将他们两个压到栈顶。

 传参完后,要执行Add函数了,执行完后需要返回这里吧,于是给了条call指令,使得执行完Add函数后能回到这里继续向下执行。

 这里在栈顶记录00c21450的地址,返回时就执行call指令的下一条指令,也就是这里的地址位置。

接下来jmp跳到Add函数里面,执行Add函数的内容。

 和main函数中一样,先准备Add函数的栈帧空间,过程相似,这里就不赘述了。

直接来看结果图:

 到这里大体和main函数栈帧创建一样。

然后Add函数里创建变量z, 将z=0放入ebp-8中。

接着是执行z=x+y,但是我们没有x和y啊,别急,其实我们之前传参的时候已经存了。

 ebp+8,ebp+0ch,其实就是从我们ebp的位置往下8个、12个字节处。因为地址是高到地的。

这两个位置就是之前传参的a和b。

将ebp+8(a)传给eax,再加ebp+0ch (b) , 最后将eax传给ebp-8 。

所以实际上,这里的ebp+8就是x,ebp +0ch就是y。

从中也可以看出,传参的时候是从右往左传的,先传的是b,再传a,因为先要将b压栈,再压a。

而且,形参不是Add函数内部创建的,而是回去找了传参的空间。

 因此,我们说改变形参的值对实参没有影响。

那么现在z的值是a+b了,要return z,怎么返回呢?怎样才能将z的值带回主函数,我们还是将其放入全局的寄存器eax中。

 因为寄存器是不会随着函数执行结束销毁的,所以先将ebp-8(z)放入eax中,再在main函数中调用就行了。

函数栈帧的销毁

返回z以后,我们又要回到main函数,那么esp和ebp是怎么回头去维护main函数的,接着来看:

        

 上面说了,pop是删除(弹出)栈顶的元素,所以这里将栈顶的edi,esi,ebx全部弹出。

然后将ebp的值赋给esp,那么此时esp就不指向栈顶位置了,而是指向此时的ebp。

再将ebp   pop弹出,那么ebp就回到了main函数的栈底位置,此时esp和ebp归位,重新维护main函数去了。

 

 到这里,大家再思考一个问题:之前在栈顶存的call指令的下一条指令00c21450是不是Add函数调用完返回后便于esp找到它继续执行main函数用的,这样就整个连起来了,所以汇编语言设计还是很巧妙的。

 执行到call指令下面,esp+8是为了销毁形参x,y  释放形参占用的空间

 这样,形参什么时候销毁,空间什么时候释放,怎么样释放,大家应该清楚了吧。

后面main函数的销毁也是一样的。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值