浅谈函数栈帧(Stack Frame)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

💙作者阿润菜菜

📖专栏C++


本文目录

什么是栈帧

 在调试中观察

总结


什么是栈帧

那我们先来看看什么是

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

预备知识

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

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

 在调试中观察

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

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

如图 

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

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

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

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

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

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

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

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

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

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

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

32位中word是两个字节dworddouble word四字节

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

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

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

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

 那Add函数是如何带回返回值的呢可以看到把[ebp-8]里的值也就是int z 放到eax里面因为这里的eax是寄存器硬件啊寄存器不会因为程序退出就销毁的相当于拿一个全局的寄存器把返回值保存起来等到执行main函数我们再把它拿出来。

那么函数怎么返回呢

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

此时esp指到ebppop一下把栈顶的元素弹出来因为里面放的是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

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


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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6