x86-64程序栈

x86 vs x86_64过程调用约定

下表是对x86寄存器和x86_64寄存器的一个使用约定的对比表, 从该表可知, 由于x86_64中的物理寄存器数量比x86的多出了一倍, 因此可以在很大程度上减轻了程序调用对栈的依赖, 并且更好地利用寄存器交换临时数据, 例如我们可以将参数直接缓存到寄存器中, 并且也可以将局部变量直接缓存到寄存器, 当然我们会耗尽所有可用的寄存器, 然后又回到x86程序栈的约定的操作方式: 参数选入栈, 需要调用该参数时又加载到寄存器中. 但是多数情况下, 我们依然尽最大努力尝试绕过程序栈直接利用寄存器, 并避免过多地使用栈内存写入.

回到我们上表, 16个通用的寄存器, 分别是8字节的位宽, 我们知道

  • 被调用者保存函数数据状态的寄存器, 分别是RBX, RBP, R12, R13, R14, R15.
  • 用于调用者函数传递参数的寄存器是RCX, RDX, RSI, RDI, R8, R9, 也就是说在理想情况下, 我们编写的调用者函数的参数控制在6个参数以内的话, 在x86_64环境中, 被调用者函数可用直接从这些寄存器中获取参数值, 而且我们知道访问寄存器的速度远远快于从调用者帧栈的参数域. 但如果我们的被调用者函数的参数个数多于6个时, 额外的参数不得不重写入栈中, 这又回到x86架构的过程调用的规定
  • 关于返回值的处理, 在大多数情况下, x86_64的过程调用依然遵循x86的传统, 依然使用RAX寄存器作为返回值, 只不过它是EAX的扩展版本而已.
  • 关于栈指针的使用规定, x86_64的过程调用依然遵循x86的传统,使用rsp寄存器指向栈顶
  • 关于局部变量的使用约定,x86_64的过程调用,若存在闲置的寄存器的话,局部变量可以直接缓存到寄存器中,若局部变量太多的情况下,再次回到x86过程调用的约定,将额外的局部变量写入栈中。
  • callq指令在写入栈的返回地址是64位的尺寸,也就是说栈指针是隐含按照指令movq -8(%rsp),%rsp移动栈指针
  • 函数可以访问%rsp之后最多128个字节的内存:“红色区域”,意味着可以在通过%rsp的来在“红色区域”内存储一些临时数据。而不必使用使用多条指令。
  • 关于帧指针的使用约定,如果深入研究过栈的话,可能大部分人会接受如下观点
    x86_64的过程调用取消了帧指针,所有对当前栈帧中的内存字段的访问引用,由%rsp进行相对寻址来实现,%rbp即被作为视为通用寄存器

严谨地所这个问题并不是所有C/C++编译器一致地遵守的, 若使用不同种类C/C++编译器那么你反编译可能会得出不同的结论.

若要支持上面约定俗成的结论需要指定以下两个前提:

  1. 用的是上面C/C++编译器
  2. 并且编译的时候执行了那些编译选项为前提.

红色区域

%rsp所指向的位置之外的128字节区域被视为已保留, 并且不得有信号或中断处理程序修改. 因此, 函数可用使用此区域存储函数调用不需要的临时数据. 尤其是, 叶子函数(就是位于整个函数调用链的末端的函数)可用在整个堆栈框架中使用此区域, 而不是在序言和结语中掉整栈指针. 这个地区被称为”红色区域(Red Zone”

需要注意是的, 红色区域会被函数调用所破坏, 因此通常在叶子函数(不调用其他函数的函数)中使用.

x86_64栈帧

通常情况下, x86-64函数不再需要栈帧, 唯一写入栈的操作就是指向到callq指令时候的”返回地址”8个字节. 者使得栈帧的结果变得非常简单. 那么上面情况下函数需要栈帧呢?

  • 局部变量太多, 64为寄存器处理不过来
  • 局部变量中存在的数值或结构体(或者叫类类型)的变量
  • 使用取值操作符&就计算变量的的内存地址
  • 调用另外一个超过6ge参数的函数
  • 需要在修改它们之前保存被调用者在保持寄存器的状态

总结

  • x86_64过程调用, 大量地使用寄存器来传递参数, 甚至保存函数的局部变量, 因为寄存器的存取速度远远高于内存访问内存写入
  • 因此这使得x86_64过程调用的每个函数的栈尺寸非常少, 甚至不需要栈.
  • 同时我们也通过详细的示例来阐述了一个观点: x86_64架构中, 在过程调用会优先86_64过程调用, 其次是使用x86过程调用约定, 就这说明了理解x86的程序栈的原理对x86_64的过程调用是非常重要的.