浅谈函数栈帧(Stack Frame)

作者:阿润菜菜

专栏:C++


本文目录

什么是栈帧

 在调试中观察

总结


什么是栈帧

那我们先来看看什么是

栈(stack)是限定仅在表尾进行插入或者删除的线性表。栈是一种数据结构,它按照后进先出的原则存储数据。把数据元素存放到栈顶时,叫压栈(push) ,从栈顶删除一个元素,叫出栈(pop)。那什么是栈帧(Stack Frame)呢?

预备知识:

 每一次函数的调用,都会在调用(call stack)上维护一个独立的栈帧空间(stack frame).每个独立的栈帧一般包括:

  • 函数的返回地址和参数
  • 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  • esp、ebp这两个寄存器中存放的是地址,这两个地址是用来确认各变量,用来维护函数栈帧的
  • .ebp(栈底指针):该指针永远指向系统栈最上面一个栈帧的底部
  • esp(栈顶指针):该指针永远指向系统栈最上面一个栈帧的栈顶
  • 栈是从高地址向低地址延伸,一个函数的栈帧用ebp 和 esp 这两个寄存器来划定范围.ebp 指向当前的栈帧的底部,esp 始终指向栈帧的顶部
  • 压栈push :esp上移朝低地址移动;出栈pop:栈顶元素弹出,esp下移高地址

 在调试中观察

 我们使用的环境是VS2013,由于函数栈帧是底层知识,而越高级的编译器越难以抽离出函数栈帧分装的过程,不容易学习和观察。同时在不同的编译器下,函数调用栈帧的创建也是略有差异的,但大体思路都是一样的。

每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。 

浅谈函数栈帧(Stack Frame)_第1张图片

如图: 

 在调用main函数的时候,会在栈中开辟一块空间,由ebp和esp共同来维护(在调用哪个函数,ebp和esp就会维护哪块空间)

但main函数是被怎么调用的呢?是被系统内提前建立好的函数栈帧调用的:

浅谈函数栈帧(Stack Frame)_第2张图片

通过反汇编可以看到main函数是被_tmainCRTStartup()函数调用的,通过一系列汇编指令调用main函数同时esp和ebp来进行维护:  我们来看下这些汇编指令走的过程

浅谈函数栈帧(Stack Frame)_第3张图片

 当执行压栈push时, ebp压到esp顶部,esp上移

执行move时,mov ebp,esp 就是把esp的地址交给edp

此时ebp和esp指向了同一个地址

浅谈函数栈帧(Stack Frame)_第4张图片

 下一步是sub  esp,0E4h 就是把esp减去0E4h使esp上移。也就是为main函数开辟了空间

浅谈函数栈帧(Stack Frame)_第5张图片

下面就是三个push:分别压进了ebx,esi和edi三个值(具体是什么值,无需关心,后面会自动弹出)

浅谈函数栈帧(Stack Frame)_第6张图片

 接下来lea (load effective address)  就是为edi加载有效地址 [ebp - 0E4h]

浅谈函数栈帧(Stack Frame)_第7张图片

通过下面mov和rep stos三个命令,我们把ebx到ebp之间的栈空间初始化为eax里的内容 

此时main函数的栈帧空间已开辟好,开始执行真正的有内容的代码:

32位中,word是两个字节,dword(double word)四字节

浅谈函数栈帧(Stack Frame)_第8张图片

mov 把0Ah(也就是10)放到ebp-8的位置上,而ebp-8实际上就是为int a开辟一个空间 (局部变量int b =20 ,c = 0 的创建 与变量a 类似)

接下来就是调用Add函数了,我们可以看到是一条mov 指令,把[ebp -14h]值(也就是变量b值)放到eax中;然后就是push,压栈eax(b =20), 下面接着一条mov和push命令,类似压栈将变量a的值压入ecx;

浅谈函数栈帧(Stack Frame)_第9张图片

 那么刚刚做的步骤是在为Add函数传参吗?是的。接下来call 命令就是调用,通过调试窗口我们可以清楚的看到a上面就是call指令的下一条指令的地址。这一步是在调用函数的同时把下一条指令的地址压上去,作为函数回归的标记

浅谈函数栈帧(Stack Frame)_第10张图片

浅谈函数栈帧(Stack Frame)_第11张图片

至此就来到我们的Add函数栈帧,与上面讲的main函数栈帧开辟一样。参数是从右向左压栈的,从上面我们也可以清楚的看到形参不是在Add函数内部创建的,而是回来到我们传参的空间,这也直接证明了形参是实参的临时拷贝这句话 !

浅谈函数栈帧(Stack Frame)_第12张图片

 那Add函数是如何带回返回值的呢?我们知道函数调用完函数栈帧会销毁的,那函数栈帧都被销毁了,Add函数怎么传给返回值呢:可以看到把[ebp-8]里的值也就是int z 放到eax里面,因为这里的eax是寄存器(硬件)啊,寄存器不会因为程序退出就销毁的,相当于拿一个(全局的)寄存器把返回值保存起来,也就是拷贝了这个变量值。等到执行main函数我们再把它拿出来。

那么函数怎么返回呢?

浅谈函数栈帧(Stack Frame)_第13张图片

 在 return z执行后,我们pop弹出,把栈顶的元素取出放到edi里面去,依次pop三次,esp指针就往下走。当我们函数调用完了那这个空间就没必要存在了,所以mov把ebp的地址给esp。

此时esp指到ebp,pop一下把栈顶的元素弹出来,因为里面放的是main函数的栈底指针,把结果弹到ebp里面去就可以瞬间到main栈底了

最后ret这条指令就是栈顶弹出call下一条指令地址然后跳过去,回来后就到了call下一条指令地方。此时add 就把形参的空间还给操作系统,然后把eax的值给[ebp-32]空间就是变量int c的空间。

觉得配合图示很难理解,大家可以结合实操快速掌握函数栈帧的创建和销毁过程

总结

在函数调用的过程中,有函数的调用者(caller)和被调用的函数(callee). 调用者需要知道被调用者函数返回值; 被调用者需要知道传入的参数和返回的地址

函数调用:

  • 参数入栈: 将参数按照调用约定(C语言是从右向左)依次压入系统栈中
  • 返回地址入栈: 将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行
  • 代码跳转: 处理器将代码区跳转到被调用函数的入口处;
  • 栈帧调整:
    1.将调用者的ebp压栈处理,保存指向栈底的ebp的地址(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置; push ebp
    2.将当前栈帧切换到新栈帧(将eps值装入ebp,更新栈帧底部), 这时ebp指向栈顶,而此时栈顶就是old ebp mov ebp, esp
    3.给新栈帧分配空间 sub esp, XXX

函数返回: 

  • 保存被调用函数的返回值到 eax 寄存器中 mov eax, xxx
  • 恢复 esp 同时回收局部变量空间 mov ebp, esp
  • 将上一个栈帧底部位置恢复到 ebp pop ebp
  • 弹出当前栈顶元素,从栈中取到返回地址,并跳转到该位置 ret

内容参考:系统栈的工作原理


 本文完。如有建议或问题欢迎评论区讨论

你可能感兴趣的:(C++,一起学习C语言,编译器,c语言,汇编)