C/C++ — 函数栈帧的简单见解

来源:互联网 发布:淘宝上卖食品需要什么 编辑:程序博客网 时间:2024/05/21 11:34

                         栈帧


 我们在进行编写程序的时候无论是定义变量,还是调用函数,甚至输出一句话都要用到内存空间,

如果说程序是鱼  的话,那么内存空间就是大海,没有内存空间什么都没有办法实现,但是程序在

内存空间中是什么样子的呢??


 


 我们可以看出来随着地址的增大 大致分为这几个区域:代码区,静态全局区,堆区,共享区,栈区。

但是今天的主题只要是栈里面的知识。 那么这个标题栈帧是什么东西呢? 好! 先听听定义(就是那

种明明很简单 就可以说出来,非要讲到你听不懂) 栈帧的定义:栈帧也叫过程活动记录,是编译器

用来实现过程/函数调用的  一种数据结构”,其实通俗易懂的说法就是:栈帧就是存储在用户栈上

的每一次函数调用涉及的相关信息,我理  解就是存放函数的一个临时的空间。

还有在栈结构中,每一个函数都有自己的他的EBP(基制寄存器也叫栈底寄存器)和ESP(栈顶寄存器)还

有pc指针存储的是CPU下次要执行的指令的地址),  他们三个的内部保存的都是地址。 

 

但是栈帧又是什么原理?它又是如何实现的呢?



 下面有一段代码:



代码本身很简单就是只是为了在汇编和内存窗口调查他们存储的方式,注意函数fun和main函数都存储在栈区,

先有main函数的栈帧,在调用fun的时候才会有它所对应的栈帧。

首先我们观察变量被压入栈中的方式,是先将ESP下移一个单位,然后将变量压入。还是先压入再下移。

我们调出来在在发生汇编语言中mov命令之前



这个是汇编语言中mov命令发生时的



所以我们可以看出,他是先pc指针下移,然后再将变量压如栈中。


首先我对他进行调试,找出他的反汇编的窗口,让我们清晰地看到,他们是如何运作的。

下面 分别是fun函数的汇编语言代码和main函数的汇编语言代码,不懂汇编没关系,我查阅了

一点资料,我来分析每一句的意思。





我们可以发现实例化的时候我们参数实例化是从右往左的。为什么?

(看第一张图倒数第七行,这个是main函数的汇编界面)



main函数汇编代码中,先将b的内容压进了栈中,所以实例化是由右往左的。

现在开始分析main函数的汇编语言,从main函数中赋值结束开始,接下来有一个call命令,

它先将当前指令的下一条指令地址保存也就是main:retaaddr,然后pc指针跳转到fun函数的

第一行代码,然后在他调用fun函数的时,在fun函数的第一个指令的时候,main函数的栈底

地址被保存在栈里接下来的位置(也就是push main函数中的ebp),这里有个细节,就是push

结束之后,esp指向保存main函数中的ebp地址的位置(这里就是fun函数的栈底,然后把esp

(main函数的栈底)中内容放到ebp里(mov ebp esp),然后现在的ebp指向了fun函数的栈

底,然后再对esp进行操作给他减去0cch个单位地址(sub esp,OCCh)此时esp的值就是(原始

的esp地址-0cch),这时pc指针指针指向fun函数的栈顶,至此fun函数的栈顶形成。



大家都知道形参是是实参的一份临时拷贝,这里我补充一点点,其实在fun函数中所用到的main函数

中的数据,他们其实还是会被拷贝一份在,main函数的栈顶,然后在fun函数的中使用该数据的话,

他们调用的是哪个拷贝出来的,不会使用原本数值,当fun函数调用结束后,这些数据就会被还给操

作系统。




红色的指向是准备创建fun函数栈帧的时候的ebp和esp指向,绿色是创建完fun函数栈帧时的sbp和esp的

指向。如果函数调用完毕那么他的返回时什么样子呢,接下来进行。


根据汇编的语言可以看出来,他是怎么消除临时变量的。


首先是mov esp ebp,把ebp的地址赋给esp,这时esp和ebp都指向main函数的栈顶。然后 pop ebp,是将

栈顶的内容(原来ebp的地址)弹出来赋给ebp里,所以这时候ebp指向了main函数的栈底。esp因为自己

的内容(原来的ebp)被pop,所以他指针上移一个单位然后是ret命令,这是一个集成命令,先是pop 

pc,把esp指向的地址(main:retaddr)给pc指针,然后pc指针从fun函数的最后一条代码跳转到(call命

令的下一条命令),然后程序继续走。走几步以后会出现add esp 8,这是我们当时为了存储a和b所开辟

的空间,现在这个命令释放掉了它。现在esp指向就是图中的紫色那条线指向的位置,他返回的时候到这

里就结束了。开辟的所有临时变量全部释放完毕。系统根据EIP寄存器里存储的地址,CPU就知道下一步

应该做什么,也就是应该执行main函数中的“函数调用结束”。EBP寄存器存储的是栈底地址,而这个地

址是由ESP在函数调用前传递给EBP的。等到调用结束,EBP会把其地址再次传回给ESP。以ESP又一次指

向了函数调用结束后,栈顶的地址。说道理就是:你在栈里申请了空间,你也一定要释放。


现在我们理解了栈帧的形成和结构,对于他的结构理解之后,可以发现它可以帮助你理解深层次的函数

问题,比如可变参数,当你在va_arg(arg,类型)时就可以为下一个参数申请出位置。还有不需要知道

栈中某个变量的地址通过别的变量和类型,修改此变量的值。这样说可能有些迷,举个例子说一下,还是

刚刚那个代码,我稍微改一下。



  
可以看出我拿到了a的地址就可以更改b的值,你信不信?什么通过p+1就可以修改b的值呢?先看看下面这张图.



所以*p+1在栈帧结构上就指向了b,然后当然就可以修改它了。


结果对不对呢。看看结果  看吧又对了~~





(不要太相信我,一定要自己去调试试试,调出地址窗口,自己调调试试,加油..)

没有问题,当然这种用法可以有很多,因为你已经了解到了结构,如果你再知道类型,岂不是想干啥就

干啥,唯一需要注意的就是返回值,不能让你的返回值破坏了原来的那种清除栈结构的体系。 具体为

什么看着栈的结构图想想就想通了。


这就是我对栈帧的一点小小的理解,思路应该没问题,可能里面有一些小错误,欢迎大家指出来。

注:一些重要的汇编语言意思


mov:把一个字节、字或双字的操作数从源位置传送到目的位置    

push:实现压入操作的指令是PUSH指令,也就是在图中,它把内容压进栈中。

pop:实现弹出操作的指令是POP指令

lea:LEA指令的功能是将源操作数、即存储单元的有效地址(偏移地址)传送到目的操作数。

call: 保存下一条指令的地址以及跳转至指定函数入口。


2 0