System V Application Binary Interface
AMD64 Architecture Processor Supplement
(With LP64 and ILP32 Programming Models)
Version 1.0
AMD64 has been previously called x86-64.
The latter name x86-64 is used in a number of places out of historical reasons instead of AMD64.
AMD64架构是x86架构的扩展。任何实现AMD64体系结构规范的处理器也将为Intel 8086体系结构的前代提供兼容模式,包括32位处理 器,如Intel 386、Intel Pentium和AMD K6-2处理器。符合AMD64 ABI的操作系统可以为执行设计为在这些兼容模式下执行的程序提供支持。AMD64ABI不适用于此类项目;本文仅适用于在AMD64架构提供的“长”模式下运行的程序。
使用AMD64指令集的二进制可以编程为32位模型,其中C数据类型int、long和所有指针类型都是32位对象(ILP32);或64位模型,其中C int类型是32位,但C long类型和所有指针类型是64位对象(LP64)。
本规范涵盖LP64和ILP32编程模型。除非另有说明,AMD64架构ABI遵循Intel386 ABI中描述的约定。AMD64 ABI并不是复制Intel386 ABI的全部内容,而是仅指出对Intel386ABI进行了更改的地方。
没有尝试为C以外的语言指定ABI。但是,假设许多编程语言都希望与用C编写的代码链接,因此这里记录的ABI规范也适用于此。
The architecture specification is available on the web at http://www.x86-64.org/documentation.
结构体和联合体以其最严格对齐的成员对齐。每个成员都被分配到具有适当对齐的最低可用偏移。任何对象的大小总是对象对齐方式的倍数。数组使用与其元素相同的对齐方式,但长度至少为16字节的局部或全局数组变量或C99可变长度数组变量始终具有至少16字节的对齐方式。结构体和联合对象可能需要填充以满足大小和对齐约束。任何填充的内容都未定义。
本节介绍标准函数调用顺序,包括堆栈帧布局、寄存器使用、参数传递等。标准调用顺序要求仅适用于全局函数。无法从其他编译单元访问的本地函数可能使用不同的约定。然而,建议所有函数尽可能使用标准调用序列。
AMD64架构提供16个通用64位寄存器。此外,该体系结构提供了16个SSE寄存器,每个寄存器128位宽,以及8个x87浮点寄存器,每个80位宽。x87浮点寄存器中的每一个都可以在MMX/3DNow!模式作为64位寄存器。所有这些寄存器对于给定线程的所有活动过程都是全局的。
Intel AVX(高级矢量扩展)提供16个256位宽的AVX寄存器(%ymm0-%ymm15)。%ymm0-%ymm15的低128位被别名到相应的128b位SSE寄存器(%xmm0-%xmm15)。Intel AVX-512提供32个512位宽的SIMD寄存器(%zmm0-%zmm31)。%zmm0-%zmm31的低128位被别名到相应的128b位SSE寄存器(%xmm0-%xmm315)。%zmm0-%zmm31的低位256位被混叠到相应的256位AVX寄存器(%ymm0-%ymm316)。对于参数传递和函数返回,%xmmN、%ymmN和%zmmN引用相同的寄存器。同时只能使用其中一个。我们使用向量寄存器来引用SSE、AVX或AVX-512寄存器。此外,Intel AVX-512还提供8个矢量掩码寄存器(%k0-%k7),每个64位宽。
本小节讨论每个寄存器的用法。寄存器%rbp、%rbx和%r12到%r15“属于”调用函数caller,被调用函数callee需要保留它们的值。换句话说,被调用函数必须为其调用者保留这些寄存器的值。剩余寄存器“属于”被调用函数。如果调用函数希望使用这样的寄存器值,则必须将该值保存在其本地堆栈帧中。
save way | save regs |
---|---|
callee save | %rbp、%rbx和%r12到%r15“ |
caller save | All regs - callee save regs |
Note:
%xmm15 - %xmm31 are only available with Intel AVX-512.
%ymm15 - %ymm31 are only available with Intel AVX-512.
Note that in contrast to the Intel386 ABI, %rdi, and %rsi belong to the called function, not the caller.
So they are caller save register.
除了寄存器之外,每个函数在运行时堆栈上都有一个栈帧。此栈从高地址向下增长。图3.3显示了栈帧结构。
输入变量区域的结尾应对齐到16(32或64,如果堆栈上传递__m256或__m512)。换句话说,当控制转移到函数入口点时,值(%rsp+8)(我觉的文档笔误,应该是%rbp+8 )总是16的倍数(32或64)。堆栈指针%rsp始终指向最近分配的堆栈帧的结尾。
Red Zone
%rsp所指位置以外的128字节区域被认为是保留的,不能被信号或中断处理程序修改。因此,函数可以将此区域用于函数调用中不需要的临时数据。特别是,叶函数可以在其整个堆栈帧中使用此区域,而不是在序言和尾声中调整堆栈指针。这个区域被称为红色区域。
这些参数要么放置在寄存器中要么存到堆栈中。值的传递方式将在以下章节中描述。
我们首先定义了一些类来对参数进行分类。这些类对应于AMD64寄存器类,并定义为:
类型(分类)定义
INTEGER :该类由适合一个通用寄存器的整数类型组成。
SSE:类由适合向量寄存器的类型组成。
SSEUP:该类由适合向量寄存器的类型组成,可以在其高位字节中传递和返回。
X87,X87UP:这些类由将通过X87 FPU返回的类型组成。
COMPLEX_X87:此类由将通过X87 GPU返回的类型组成。
NO_CLASS:该类用作算法中的初始值设定项。它将用于填充、空结构体和union。
MEMORY:这个类表示通过堆栈在内存中传递和返回的类型。
参数分类 (基本类型)
每个参数的大小取整到8字节
1) (signed and unsigned) _Bool, char, short, int, long, long long, and pointers 分到 INTEGER。
2)float, double, _Decimal32, _Decimal64 and __m64 分到 SSE。
3) __float128, _Decimal128 and __m128 分两半,低半部分分到SSE,高半部分分到SSEUP。
4)__m256/__m512 安8字节为单位分成4/8份,最低的一份划入SSE,其他部分分到SSEUP。
5)fp80 (long double)中的64bit尾数分到X87,16bit 的exp加pading 分到X87UP。
6)__int128也被分到INTEGER,需要2个通用寄存器。实际上,它被看作一个结构体:
typedef struct {
long low, high;
} __int128;
唯一的不同是它要求16字节对齐(store的时候)。
7)复数类型参数 complex T (float/double)被视为
struct complexT {
T real, imag;
};
8) complex log double (fp80) 被单独分到COMPLEX_87
参数分类 (聚合类型)
结构体,数组安以下规则分类:
1)若一个对象(object)>8字节,或者它包含非对齐的域(unaligned fields)则被划入MEMORY。
2)若一个聚合体(结构体,数组)超过8字节,则每个单独分类,每个8字节初始化为NO_CLASS
(原文:If the size of the aggregate exceeds a single eightbyte, each is classified separately. Each eightbyte gets initialized to class NO_CLASS.)
3) If a C++ object is non-trivial for the purpose of calls, as specified in the C++ ABI 13, it is passed by invisible reference (the object is replaced in the parameter list by a pointer that has class INTEGER)
4) Each field of an object is classified recursively so that always two fields are considered. The resulting class is calculated according to the classes of the fields in the eightbyte:
(a) If both classes are equal, this is the resulting class.
(b) If one of the classes is NO_CLASS, the resulting class is the other class.
© If one of the classes is MEMORY, the result is the MEMORY class.
(d) If one of the classes is INTEGER, the result is the INTEGER.
(e) If one of the classes is X87, X87UP, COMPLEX_X87 class, MEMORY is used as class.
(f) Otherwise class SSE is used.
5.)关于类型合并 Then a post merger cleanup is done:
(a) If one of the classes is MEMORY, the whole argument is passed in memory.
(b) If X87UP is not preceded by X87, the whole argument is passed in memory.
© If the size of the aggregate exceeds two eightbytes and the first eightbyte isn’t SSE or any other eightbyte isn’t SSEUP, the whole argument is passed in memory.
(d) If SSEUP is not preceded by SSE or SSEUP, it is converted to SSE
参数传递
MEMORY:栈传参。
INTEGER :安照顺序 %rdi, %rsi, %rdx, %rcx, %r8和%r9 传参。
SSE:按照顺序 %xmm0 ~ %xmm7 传参。
SSEUP:高位部分传入SSE可用的下一个寄存器。
X87,X87UP,COMPLEX_X87:栈传参。
Note:1 bit的_Bool在传递到内存或寄存器中时,0 bit 存值,1-7bits需要清0。
传参寄存器不够用
如果已经没有传参寄存器可用了,那么整个参数将传递到栈上。这意味着如果该参数的部分已经存到寄存器上,那么也退出来,保证整个参数都存在栈上。
栈传参存放顺序
栈传参时我们按照从右到左的顺序来存,即先存最右边的参数再存左边的。
变参
For calls that may call functions that use varargs or stdargs (prototype-less calls or calls to functions containing ellipsis (. . . ) in the declaration) %al18 is used as hidden argument to specify the number of vector registers used. The contents of %al do not need to match exactly the number of registers, but must be an upper bound on the number of vector registers used and is in the range 0–8 inclusive.
When passing __m256 or __m512 arguments to functions that use varargs or stdarg, function prototypes must be provided. Otherwise, the run-time behavior is undefined.
返回值
和传参类似,返回值也按照返回数据类型进行分类。
1) 如果时MEMORY类型,caller为返回值分配空间,并将地址存入%rdi (就好像传递第一个参数一样,隐式的第一个参数)传给callee。当返回时,再把该地址copy到%rax (原%rdi中的地址)。
2)如果返回的是INTEGER类型,则返回值放到%rax,%rdx中。(如果只需要一个就只存%rax)
3) SSE 和INTEGER类似,对应返回寄存器为 %xmm0, %xmm1。
4) If the class is SSEUP, the eightbyte is returned in the next available eightbyte chunk of the last used vector register.
5) If the class is X87, the value is returned on the X87 stack in %st0 as 80-bit x87 number.
6) If the class is X87UP, the value is returned together with the previous X87 value in %st0.
7) If the class is COMPLEX_X87, the real part of the value is returned in %st0 and the imaginary part in %st1.
举例
typedef struct {
int a, b;
double d;
} structparm;
structparm s;
int e, f, g, h, i, j, k;
long double ld;
double m, n;
__m256 y;
__m512 z;
extern void func (int e, int f,
structparm s, int g, int h,
long double ld, double m,
__m256 y,
__m512 z,
double n, int i, int j, int k);
func (e, f, s, g, h, ld, m, y, z, n, i, j, k);
General Purpose Registers | Floating Point Registers | Stack Frame Offset |
---|---|---|
%rdi: e | %xmm0: s.d | 0: ld |
%rsi: f | %xmm1: m | 16: j |
%rdx: s.a,s.b | %ymm2: y | 24: k |
%rcx: g | %zmm3: z | |
%r8: h | %xmm4: n | |
%r9: i |
未完待续