函数栈帧的创建与销毁(汇编代码角度详解)

目录

背景介绍

main函数内部

Add函数内部

结语


在C语言的学习中,函数章节会给很多人带来疑惑:

局部变量是怎么创建的?

如何理解形参是实参的一份临时拷贝?

函数是怎么调用的?

......

而这些问题都指向了同一个知识点:函数栈帧

背景介绍

我们在调用函数的时候,都会在内存里的栈区上开辟一块空间,而这块空间,我们就称之为是该函数的函数栈帧。如下,假设我们有一个函数叫Add,那么如图所示的空间就叫做Add函数的函数栈帧

而在正式开始介绍之前,我们还需了解两个主要的寄存器:esp 和 ebp,这两个寄存器中放的是地址,而这两个地址是用来维护函数栈帧的。

我们这么来理解,一个函数的创建需要在内存上面开辟空间,而这块空间就是由esp和ebp来共同维护的,两者中间的那块空间就是正在执行的函数的函数栈帧。而esp又被称为栈顶指针,ebp又被称为栈底指针

main函数内部

注:不同的编辑器底下出现的效果也不同,此处使用的是VS2013

我们先来看这么一段代码:

int Add(int a, int b)
{
	int z = 0;
	z = a + b;
	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函数的函数在这个编辑器底下的名字是__tmainCRTStartup,这个知道就行,与我们今天的主题并无太大关联

接下来,我们就来仔细研究一下函数栈帧

我们要进入研究,只需要通过F10进入调试,随后右击鼠标转到反汇编代码就可以进行查看,如下

首先,我们在调用main函数之前需要先调用__tmainCRTStartup函数,也就是需要先为该函数在内存上开辟一块空间,如下

我们来看看下一步是怎么进行的

先是push了一个ebp在上面,push就是压栈,相对的,pop就是出栈。如下(此处__tmainCRTStartup的函数栈帧缩写为M,下同)

而接下来是move        ebp,esp   这句话的意思就是:将esp赋给ebp,在这一步之后,ebp和esp将指向同一个位置

在往下看: sub           esp,0E4h  这句话的意思是令esp向上走0E4(sub是减,而内存中下面是高地址,越往上地址越小), 这一步可以理解为正式在为main函数开辟空间了,如下

之后我们会看到三个连续的push:

因为与我们今天的主题无太大关系,所以这三步我们不做讨论,只需要知道压栈压了三个东西上去就行了

接下来是这四步

我们可以看到,其先是将ebp-0E4h的地址放进edi里面去了(lea的全称是load effective address,也就是加载有效地址),而ebp-0E4h是什么?不就是main函数的函数栈帧最上方的地址吗,也就是在连续三次push之前esp所指向的位置的地址

再来看,接下来的三句是先把39h放进ecx里,再把0cccccccch放进eax里面去,而最终产生效果的是最后一句话,这句话是什么意思呢:

把从edi指向的位置开始的,向下39h次的dword的内容全部改成0cccccccch

edi指向的位置我们刚才说过了,就是ebp-0E4h,word是两个字节,而dword中的d代表的是double,也就是四个字节。这句话我们可以这么理解:就是把main函数的函数栈帧全部初始化为0cccccccch,如下

到了这一步,我们才算是正式进入函数内部,前面的都是main函数的相关步骤

接下来我们一起来看看下一步是什么:

在此处我们会发现有符号我们不好观察,那我们就右击鼠标将显示符号名给去掉,接下来我们就会得到下图:

第一条汇编指令是  move     dword  ptr  [ ebp - 8 ],0Ah    也就是将0Ah放进ebp-8这个位置里,而0Ah是一个16进制数,翻译成10进制就是10,简单来理解就是将a放进main函数的函数栈帧里 

接下来的 b 和 c 也是一样的,如下图

由此,我们的局部变量是怎么创建的呢?我们先在内存上为该函数开辟一块函数栈帧,随后将局部变量放进该函数的函数栈帧内,这,就是局部变量的创建方式

Add函数内部

进行完这三步,我们再往下看:

我们先来仔细看看前四步:

我们会看到,move是先将ebp-14h这个地方里的东西放到eax里面去,随后将eax给push(压栈)到上面,而ebp-14h是什么?不就是 b 吗!

再往下看,将ebp-8给放到ecx里面去,然后将ecx给push(压栈)到上面,而ebp-8是什么?不就是 a 吗!

这么一看,我们分别将 b 和 a 的值压栈压在了上面,这可不就是形参吗!所以形参是实参的一份临时拷贝这句话有没有错?一点问题没有,如下:

这四步执行完之后我们会看到一个call,这个call会将这条指令的下一条指令的地址压栈压到上面,并且跳到函数内部

如上图,其会将00c21450压栈压到上面,并且跳转到Add函数内部(跳到00c210E1处),当我们执行完Add函数内部的指令之后,通过pop(出栈)00c21450这个地址我们就能找回来,如下图

当我们执行完call之后,我们需要按F11才能进入函数内部,如下:

相信看到这里的时候,你会发现:前几步和main函数的一模一样。先push一个ebp

注意,此时的ebp指向main函数下方,在此处push的作用是pop(出栈)后ebp能找回main函数

再将esp的值赋给ebp使其都指向当前esp的位置,随后将esp向上移动0cch个距离,相当于是给Add函数在栈上开辟了一块空间。接着push三个寄存器ebx、esi、edi,最后将整个Add函数的函数栈帧初始化为0cccccccch,如下:

当我们为Add函数开辟完空间之后,就该开始执行Add函数内的代码了

如上,其先是在ebp-8的位置放了一个 z,z 的值为0。

接着,(mov)将ebp+8内的数字放进寄存器eax中,(add)将ebp+0ch内的值与eax内的值相加,(mov)最后将eax内的值放到ebp-8的位置上

我们来仔细看看,ebp+8不就是a的形参吗?ebp+0ch(0c就是十进制的12)不就是b的形参吗?我们将其放进寄存器eax中进行相加,最后将算出来的值放进ebp-8也就是 z 里面。

我们发现,整个过程与main函数内的变量a、b是完全无关的,这也就解释了为什么当我们传值调用时,改变形参并不影响实参

接着我们看到 return z 的部分

这句话的意思是:(mov)将ebp-8(也就是z)位置内的数赋值给寄存器eax,放到寄存器后就安全了,即使函数被销毁了,这个数字也能被保留在寄存器中

注意注意!!接下来的部分相当重要!!

我们会看到这里先是连续三次的pop(出栈),(mov)接着将ebp的值赋给esp,相当于是让esp向下移动至ebp的位置,随后pop一下ebp,注意,此处的ebp内放的是什么?放的是指向main函数下方的地址啊!如果我们此时将其pop一下的话,就相当于让ebp从Add函数下方指向了main函数的下方,同时也相当于把Add函数的函数栈帧给销毁了。如下(虚线代表已被销毁):

然后就是ret,它代表的就是要回去,回哪里去?我们在创建Add函数的函数栈帧之前,除了压了一个ebp之外,我们是不是还压了一个call指令的下一个指令的地址啊,如上是00c21450,当我们ret之后,就会找到这个地址并执行这个处在这个地址的指令。如下,指向的是add:

也就是说,ret之后就从Add函数内部回到call的下一条指令:add

这条语句的意思是:将esp的数值加8,而我们又知道在内存中,高地址在下,低地址在上,所以简单点理解就是:将esp向下移动8。而esp+8之后也就意味着两个形参没有被维护了,所以就会被销毁。

综上,这一步就相当于是销毁两个形参。这也能说明为什么改变形参不影响实参,因为形参是自己开辟了一块空间且会被销毁,所以指向形参的指针也会变成野指针。

那有人可能就会问了:z不也被销毁了吗?那我们怎么接收函数的返回值啊?别忘了,我们在销毁 z 之前已经将存在 z 的值给存在eax这个寄存器上了,接往下看看编辑器是怎么做的

这条指令代表的是将eax中的值赋值到ebp-20h这个地址处,而ebp-20h就是 c 的地址。也就是说,我们将存在寄存器中的值放到了 c 里面去,寄存器又不会随着 z 的销毁而销毁,c 就是这样接收到Add函数的返回值的

至此,我们关于函数栈帧的创建于销毁就讲完了

结语

学完了函数栈帧之后,我们对函数这个章节的了解会来到一个全新的高度。作为示范,今天讲的只是一个简单的Add函数的函数栈帧,如果各位有兴趣的话可以在自己的编辑器上面试一试,但是请注意!!此处的环境是VS2013,不同编辑器的反汇编代码也有所不同,望周知。

祝我今天18岁生日快乐!!!

THANKS TO MY FAMILY AND MY PRINCESS ! ! !

                                                                                                                                          —— 11.17

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值