跳至主要內容

栈帧(Stack Frame)

西风逍遥游大约 4 分钟

栈帧(Stack Frame)

运行时内存布局中,栈帧是一个重要的概念。我们知道,函数是程序的基本组成单元,函数运行时,系统会为它分配临时栈内存,用来存储临时变量,传递函数调用参数,保存运行状态,这个临时栈内存在一个函数调用下的结构就是栈帧。

栈帧结构

下图是函数运行时,函数相关数据在内存上的布局情况。

  • 函数运行时,系统会为它分配临时栈内存。
  • 函数运行时,相关数据除了保存在内存(栈内存空间),但也需要通过寄存器传递或保存数据。栈内存的生命期与当前函数的生命期一样。
  • 栈大小一般情况下是(%ebp/%rbp)函数基地址和 (%esp/%rsp) 函数栈顶地址之间的内存块。
    • 有时候不一定需要函数栈顶地址,因为它可以通过函数基地址结合变量等数据计算出来。
  • 栈内存保存数据:函数参数数据副本,函数内部临时变量数据,函数基地址,函数调用者运行的指令地址等等。

x86 调用约定

这是一个32位平台。 堆栈向下生长。 函数的参数以相反的顺序传递到堆栈上,以便第一个参数是被压入堆栈的最后一个值,然后将成为堆栈上的最小值。 可以通过修改被调函数的参数来修改在堆栈上传递的参数。 使用call指令来调用函数,该指令将下一条指令的地址压入堆栈并跳转到操作数。 函数使用ret指令返回调用者,该指令从堆栈中弹出一个值并跳转到该值。 在调用call指令之前,堆栈是16字节对齐的。

函数保留寄存器ebx,esi,edi,ebp和esp; 而eax,ecx,edx是暂存器。 返回值存储在eax寄存器中,或者如果返回值是64位的,则高32位进入edx,低32位进入eax。 被调函数将ebp推入堆栈,这样紧挨着主调函数栈帧的栈顶,即此时caller-return-eip位于ebp上方4个字节处,然后将ebp设置为已保存ebp的地址。 这允许遍历现有堆栈帧。 通过指定-fomit-frame-pointer GCC选项可以消除此问题。

作为特殊的例外,GCC假定堆栈未正确对齐,并在输入main或在函数上设置了属性((force_align_arg_pointer))时将其重新对齐。

x86-64 调用约定

Quickview of registers

堆栈向下生长。 x64的调用约定只有一种,遵守system v ABI的规范。但是Linux和windows却有一些差别。在windows X64中,前4个参数通过rcx,rdx,r8,r9来传递,其余的参数按照从右向左的顺序压栈。在Linux上,则是前6个参数通过rdi,rsi,rdx,rcx,r8,r9传递,其余的参数按照从右向左的顺序压栈。可以通过修改被调用函数的参数来修改在堆栈上传递的参数。 使用call指令来调用函数,该指令将下一条指令的地址压入堆栈并跳转到操作数。 被调函数使用ret指令返回调用者,该指令从堆栈中弹出一个值并跳转到该值。 在调用调用指令之前,堆栈是16字节对齐的。

函数保留寄存器rbx,rsp,rbp,r12,r13,r14和r15; rax,rdi,rsi,rdx,rcx,r8,r9,r10,r11是暂存寄存器。 返回值存储在rax寄存器中,或者如果它是128位值,则高64位进入rdx。 可选地,被调函数推入rbp,以使caller-return-rip在其上方8个字节,并将rbp设置为已保存的rbp的地址。 这允许遍历现有堆栈帧。 通过指定-fomit-frame-pointer GCC选项可以消除此问题。

信号处理程序在同一堆栈上执行,但是在将任何内容压入堆栈之前,会从堆栈中减去称为红色区域的128个字节。 这允许小的叶子函数使用128字节的堆栈空间,而无需通过从堆栈指针中减去来保留堆栈空间。 众所周知,红色区域会给x86-64内核开发人员造成问题,因为在调用中断处理程序时,CPU本身并不尊重红色区域。 由于ABI与CPU行为相矛盾,这会导致微妙的内核损坏。 解决方案是使用-mno-red-zone或通过在内核模式下在当前堆栈以外的其他堆栈上处理中断来构建所有内核代码(从而实现ABI)。