[翻译]X86-64下的栈帧布局简介
翻译:稻天
原文http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64
转载请注明出处
数月前,我写过一篇文章《Where the top of the stack is on x86》,文章旨在厘清x86环境下一些被误解的栈使用场景。文章中描述了一个典型函数调用的图表用以展示呈现栈帧布局。
这篇文章里,我将探究x86框架下的64位版本的栈帧布局,并主要关注Linux和其他遵循System V AMD64 ABI调用约定的操作系统。Windows下的ABI会稍有不同,文章最后会简要的谈及。
●
[*]丰富的寄存器
x86只有8个通用寄存此供使用(eax, ebx, ecx, edx, ebp, esp, esi, edi)。x64将它们扩展为64位(前缀由“e”变更为“r”),并且新增了8个寄存器(r8, r9, r10, r11, r12, r13, r14, r15)。由于x86下一些寄存器有特殊的用途和意义而不是真正意义上的“通用”(尤其是ebp和esp寄存器),使得这些新增的特性产生的效用远不止是变大变多。
●
[*]参数传递
从ABI来看,函数开始的6个整数或指针类型的参数将通过寄存器传递参数,第一个参数保存在rdi中,第二个保存在rsi中,接下来依次保存在rdx,rcx,r8,r9。从第7个参数开始,接下来的所有参数都将通过栈传递。
●
[*]栈帧
有了以上的基础知识,让我们来看一下C函数的栈帧布局:
long myfunc(long a, long b, long c, long d,
long e, long f, long g, long h)
{
long xx = a * b * c * d * e * f * g * h;
long yy = a + b + c + d + e + f + g + h;
long zz = utilfunc(xx, yy, xx % yy);
return zz + 20;
}下面就是栈帧布局:

从图中不难看出,最开始的前六参数个是通过寄存器传递的,后面的两个参数传递的情形就和x86下无二了,除了那个奇怪的“红灯区”(red zone),上述的这些到底是什么?
●
[*]“红灯区(red zone)”
首先,我转述一下来自AMD64 ABI 中的定义:
%rsp指针以下的128字节的区域保留不用,且不可被信号量和中断处理程序修改。因此,只要不是函数间互相调用,函数就可以使用这片区域存储临时数据。尤其是:“末端函数”(不在调用其他函数的函数)可能使用这片区域作为函数整个栈帧的存储区。
简单的说,“红灯区”只是一个可选区域,程序代码使用时可以认为%rsp指针以下的128字节的区域不会被非同步的被信号量和中断处理程序覆写,因此程序代码可以随意使用这片区域操作数据而不必专门移动栈指针。上一句话的意思是当使用“红灯区”时,优化掉了%rsp指针的递减和重新填充的两条指令。
但是,你需要记住一点,“红灯区”会被程序覆写,因此,这块区域最有用的时候是末端函数使用的时候。
回顾一下myfunc函数在示例代码中调用utilfunc函数的情景,我们估计吧代码写成上面的形式,myfunc就不会是“末端函数”,编译器也就不会使用“红灯区”。再看utilfunc函数的代码:
long utilfunc(long a, long b, long c)
{
long xx = a + 2;
long yy = b + 3;
long zz = c + 4;
long sum = xx + yy + zz;
return xx * yy * zz + sum;
}毫无疑问,这个函数是一个“末端函数”,我们看一下gcc编译器下的栈帧布局:

由于utilfunc函数只有3个参数,因此调用该函数时不需要使用栈(仅使用寄存器就够了)。另外,由于该函数是一个“末端函数”,gcc编译器使用了“红灯区”作为函数所有局部变量的存储区,这样,%esp就无需为了分配局部变量存储区而上下移动了。
●
[*]节约通用寄存器
作为一个贯穿函数执行全程的栈帧的起点之“锚”的基址指针%rbp(x86下的ebp),给手写汇编代码和调试带来了极大的便利。但是不久前大家注意到编译器生成的汇编代码并不需要这个“锚”(编译器根据%rsp就能轻松定位数据),并且DWARF(Debugging With Attributed Record Formats)调试信息格式支持处理无基址指针的方法(CFI)。这就是为什么一些编译器开始在高级优化中省略基址指针了,这样做缩减了程序执行的“预处理代码”(prologue)和“后处理代码”(epilogue),节省出来一个通用寄存器供程序使用(在x86架构有限的GPRs资源条件下非常有用)。GPRS:GeneralPurpose Registers
x86下gcc默认是保留基址指针的,但是提供了-fomit-frame-pointer优化参数选项。对于是否推荐使用这个选项的争议非常大,如果你对此感兴趣,不妨Google一下。
总之,AMD64ABI介绍中的另一个新颖点就是让基址指针成为明确的可选项,具体来说:
通过使用%rsp索引栈帧的方法避免了传统的%rbp使用方法,这项技术节约掉了“预处理代码”(prologue)和“后处理代码”(epilogue)中的两条指令,而且也空出来一个通用寄存器供给程序使用。X64下,gcc坚持推荐这一技术并将它设置为默认选项,同时提供-fno-omit-frame-pointer选项,只要设置了这个标记,编译器就不会再省略栈帧了。
●
[*]Windows x64 ABI
X64下,Windows 实现了一套和AMD64ABI有些许不同的ABI,接下来我简要介绍一下Windows x64 ABI,谈谈它和AMD64的区别:
[*]只有4个整型/指针型参数通过寄存器传递(rcx,rdx,r8,r9)。
[*]没有所谓的“红灯区”(red zone)的概念,事实上,ABI明确声明%rsp以外的区域是不安全的,操作系统、调试器或者中断处理程序都有可能覆写这一区域。
[*]函数调用者会预先为每一个被调函数的栈帧开辟一个“寄存器参数区”,当一个函数被调用时,在分配用以保存返回地址的内存之前,会开辟一个4*8字节的区域,该区域可以被“被调函数”直接使用,无需主动分配,这对于可变参数函数和调试(为寄存器参数提供存储位置,这样,原来用于传递参数的寄存器就可以复用了)都非常有用。尽管这片4*8的区域最初的考虑是分配给寄存器传递的那四个参数的,现在的编译器将它用作优化之用了(例如:如果一个程序需要给他的局部变量分配内存,而局部变量所占内存又少于32个字节,编译器就会将这片区域用给程序用,这样,编译器就不比在挪动%rsp指针了)。
要获取关于Windows x64 ABI的更多信息,下面的链接会对你有帮助:
? Official MSDN page on x64 software conventions - well organized information, IMHO easier to follow and understand than the AMD64 ABI document.
? Everything You Need To Know To Start Programming 64-Bit Windows Systems - MSDN article providing a nice overview.
? The history of calling conventions, part 5: amd64 - an article by the prolific Windows programming evangelist Raymond Chen.
? Why does Windows64 use a different calling convention from all other OSes on x86-64? - an interesting discussion of the question that just begs to be asked.
? Challenges of Debugging Optimized x64 code - focuses on the "debuggability" (and lack thereof) of compiler-generated x64 code.
