尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

一文带你搞懂:函数栈帧的创建与销毁

一文带你搞懂:函数栈帧的创建与销毁 前言本篇文章将会带你搞懂函数栈帧的创建与销毁会说明函数栈帧有什么用并且能够更好理解局部变量是如何创建的为什么局部变量不初始化内容是随机的函数调用时参数是如何传递的传参的顺序是怎样的函数的形参和实参分别是怎样实例化的函数的返回值是如何带回的以及理解函数栈帧能解决什么问题这些问题都将一一解决。那么话不多说。什么是函数栈帧我们在写C语言代码的时候经常会把一个独立的功能抽象为函数所以C程序是以函数为基本单位的。那函数是如何调用的函数的返回值又是如何带回的函数参数是如何传递的这些问题都和函数栈帧有关系。函数栈帧stack frame就是函数调用过程中在程序的调用栈call stack所开辟的空间。这些空间是用来存放1.函数参数和函数返回值2.临时变量包括函数的非静态的局部变量以及编译器自动生产的其他临时变量3.保存上下文信息包括在函数调用前后需要保持不变的寄存器。那么接下来就走进函数栈帧的创建与销毁的过程中。函数栈帧的创建与销毁解析什么是栈栈stack是现代计算机程序里最为重要的概念之一几乎每一个程序都使用了栈没有栈就没有函数没有局部变量也就没有我们如今看到的所有的计算机语言。为什么这么说呢看看它的作用。栈主要用于1.存储局部变量函数内部定义的临时变量。2.保存函数调用信息比如函数执行完毕后应该返回到哪一行代码。栈的操作遵循先进后出的原则。在计算机系统中栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中也可以将数据从栈顶弹出。压栈操作使得栈占用的空间增大而弹出操作使得栈占用的空间减小。在经典的操作系统中栈总是向下增长由高地址向低地址的。为什么会是向下增长的呢以及为什么要这样设计呢“向下增长”的具体过程是1.起始位置当一个程序开始运行时操作系统会给它分配一个栈空间。这个栈的起始位置在内存的高地址处。一个叫做“栈指针”的寄存器会指向这个位置。2.分配空间压栈当程序调用一个函数需要为它的局部变量分配空间时栈指针会减小从高地址处挪到低地址处例如从 0xFFFF 移动到 0xFFF8。3.释放空间弹栈当函数执行完毕它的空间被释放时栈指针会增加回来从0xFFF8移动到0xFFFF。另外这个栈指针的寄存器在 32 位的 x86 架构中这个栈指针寄存器是 ESP在64位的x86架构主流中是RSP这两在汇编语言中作用完全相同。所以栈总是向下增长的。那为什么要这样设计呢主要有一下几点1.与“堆”协同工作程序运行时内存中还有另一块重要的区域叫“堆”Heap用于动态分配内存如使用 new 或 malloc。堆通常从低地址开始向高地址增长。让堆和栈从内存的两端相向生长可以最大化地利用整个内存空间直到它们相遇从而避免了中间出现无法使用的空洞。2.硬件优化CPU 为“栈指针”的加减操作做了专门的硬件优化。无论是分配还是释放栈空间都只需要简单地移动栈指针速度极快这对于频繁发生函数调用的程序来说至关重要。这样设计就能高效管理局部变量和函数调用。下面这张内存布局图可以更直观地展示栈与堆的相对增长方式内存空间压栈栈指针下移分配堆指针上移高地址0xFFFFFFFF栈区向下增长栈指针 RSP/ESP 随压栈减小空闲区栈与堆相向生长直到相遇堆区向上增长由 new / malloc 动态分配低地址0x00000000从图中可以看到栈从高地址向低地址“向下增长”堆从低地址向高地址“向上增长”两者相向而行从而最大化利用内存空间。相关寄存器和汇编指令相关寄存器eax通用寄存器保留临时数据常用于返回值ebx通用寄存器保留临时数据ebp栈底寄存器esp栈顶寄存器eip指令寄存器保存当前指令的下一条指令的地址相关汇编命令mov数据转移指令push数据入栈同时esp栈顶寄存器也要发生改变pop数据弹出至指定位置同时esp栈顶寄存器也要发生改变sub减法命令add加法命令call函数调用1. 压入返回地址 2. 转入目标函数jump通过修改eip转入目标函数进行调用ret恢复返回地址压入eip类似pop eip命令函数栈帧的创建与销毁过程每一次函数调用都要为本次函数调用开辟空间就是函数栈帧的空间。这块空间的维护是使用了2个寄存器 rsp 和 rbp rbp 记录的是栈底的地址 rsp 记录的是栈顶的地址。如图所示接下来就用编译器来更加直观的理解吧。测试代码如下#includestdio.hintAdd(intx,inty){intz0;zxy;returnz;}intmain(){inta3;intb5;intret0;retAdd(a,b);printf(%d\n,ret);return0;}这里用的是VS2026。进入调试窗口后有个调用堆栈这里显示外部代码。然后这里应该就有反汇编窗口就能看到这些了。接下来我们来看看这个是如何改变栈的。sub rsp,108h这个是让栈顶指针rsp减去108h(264push rbppush rdi这两个指令中push是压栈指令执行这个指令CPU会将栈顶指针的值减去一个单位64位系统是8个字节再将数据放入栈顶。这是栈顶指针向低地址增长的过程。而rbp和rdi是两个64位通用寄存器在这里它们的作用完全不同。rdi:这里push rdi是将函数传过来的实参推入栈中保存为什么要这样rdi 是第一个函数参数的存放处。如果这个函数内部还要调用其他函数rdi 可能会被覆盖掉。所以编译器在函数刚开始时赶紧把它的值存到栈里“备份”起来以免丢失。rbp:这里rbp是一个栈底指针有了这个我们再加上偏移量就能直到某些变量的地址了。下面用图解的方式一步步展示这 5 行汇编代码执行后 rsp 的变化和栈内存的布局第5步lea rbp, [rsp20h]rsp 0x00000000000FFEE8不变rbp rsp 0x20 0x00000000000FFF08第4步sub rsp, 108hrsp 0x00000000000FFEE8-264字节为局部变量开辟空间第3步push rdirsp 0x00000000000FFFF0-8字节rdi 的值压入栈顶第2步push rbprsp 0x00000000000FFFF8-8字节rbp 的值压入栈顶第1步mov dword ptr [rsp8], ecxrsp 0x0000000000100000不变将 ecx 的值写入 rsp8 处初始状态rsp 0x0000000000100000栈顶再结合栈内存布局来看每一步的变化栈内存布局高地址在上高地址 0x0000000000100000[rsp8] 处存放 ecx 的值第1步写入rbp 的备份第2步 push rbp 压入rdi 的备份第3步 push rdi 压入sub rsp, 108h 开辟的 264 字节空间第4步用于存放局部变量 z 等rbp 指向这里rsp 0x20第5步低地址 0x00000000000FFEE8当前 rsp从图中可以清晰地看到前 3 步中push rbp和push rdi各让 rsp 向下移动 8 字节第 4 步sub rsp, 108h一次性让 rsp 向下移动 264 字节为局部变量腾出空间第 5 步lea rbp, [rsp20h]让 rbp 指向栈帧内部的一个固定位置之后就可以通过rbp 偏移量来访问局部变量了。int z 0;这个代码被编译器翻译为了mov dword ptr [z], 0。这里的 [z] 并不是一个全局变量它其实是相对于当前栈帧基址的偏移量。编译器会把 z 的值存放到刚才通过 sub rsp, 108h 分配出来的栈空间里。为什么局部变量不初始化值是随机的mov dword ptr [z], 0在这个指令里编译器明确的在z这个地址里存入了0那么z就不会是随机数就是0.再看几张图直观理解一下。这里就没有这个指令我再将z初始化为其它值试试。可以看到这里就是mov dword ptr [z], 1了这里就是初始化为1了这样就直观理解了为什么为什么局部变量不初始化值是随机的函数参数是通过rdi这种寄存器传递过来的还有其它的寄存器传递参数rdi只传函数的第一个参数。第 1 个参数 - rdi第 2 个参数 - rsi第 3 个参数 - rdx第 4 个参数 - rcx第 5 个参数 - r8第 6 个参数 - r9编译器会优先使用寄存器来传递参数因为这样速度极快不需要去读写内存。如果有更多的参数需要去传递那么多出来的参数就需要从右往左依次push到栈中。这些就是寄存器操纵栈内存的过程。结束语那么本篇文章到此就结束了若对你有所帮助还请多多点赞关注支持一下若有什么错误的地方或者有遗漏的还请不吝赐教。
返回列表