目录
1. X64调用规则概述
2. 开启X64优化
2.1 优化选项说明
2.2 Visual Studio开发环境中设置这个编译选项
3. 类型和存储
3.1 标量类型(Scalar types)
3.2 聚合类型(Aggregates types)和联合类型(Union type)
3.2.1 数组
3.2.2 结构
3.2.3 联合
3.3 结构对齐示例(以C代码示例)
3.4 位域(Bitfields)
4. 与X86编译器的冲突
5. X64寄存器用法
6. X64内存栈的用法
6.1 X64内存栈的分配
6.2 X64内存动态参数栈区域构建
6.3 函数类型
6.4 malloc对齐
6.5 alloca函数
7. prolog(序言)和epilog(完结)
8 X64异常处理
9 内置(intrinsic)和内联(inline)汇编
10 X64映像格式
本节描述X64架构(X86架构的64位扩展)的基本应用程序二进制接口(Application Binary Interface,简记为ABI)。这个主题的内容包括函数调用规则、类型分布、栈和寄存器用法、以及其它内容。
X64与X86之间重要的两个区别是64位的寻址能力和一套平坦的16个16位通用寄存器。给定扩展的寄存器集,X64使用 __fastcall调用规则和基于RISC(Reduced Instruction Set Computer)的异常处理模型。__fastcall调用规则采用寄存器来传递被调函数的前4个参数,以及采用栈空间传递被调函数的超出4个以外的参数。关于X64函数调用规则,包括寄存器的使用、栈参数、返回值、以及栈的解开(stack unwinding),参见X64调用约定。
下面的编译选项可以帮助你优化你写的X64代码:
/favor (对具体架构的优化):该选项生成针对特定架构或 AMD 和 Intel 架构中的微架构细节进行优化的代码。
语法形式:/favor:{blend | ATOM | AMD64 | INTEL64}
说明:
/favor:blend
(x86和x64)生成的代码针对AMD 和 Intel 架构中的微架构细节进行了优化。虽然 /favor:blend可能无法在特定处理器上提供最佳性能,但它旨在为各种 x86 和 x64 处理器提供最佳性能。 默认情况下,/favor:blend 有效。
/favor:ATOM
(x86 和 x64)生成的代码针对Intel Atom处理器和Intel Centrino Atom处理器技术的细节进行了优化。使用 /favor:ATOM生成的代码还可以为Intel处理器生成Intel SSSE3、SSE3、SSE2 和 SSE 指令。
/favor:AMD64
(仅限 x64)为支持 64 位扩展的AMD Opteron和Athlon处理器优化生成的代码。优化后的代码可以在所有 x64 兼容平台上运行。使用/favor:AMD64 生成的代码可能会导致支持Intel64的Intel处理器的性能下降。
/favor:INTEL64
(仅限 x64)为支持Intel64的Intel处理器优化生成的代码,这通常会为该平台带来更好的性能。 生成的代码可以在任何 x64 平台上运行。使用 /favor:INTEL64 生成的代码可能会导致AMD Opteron和支持64位扩展的Athlon处理器的性能下降。
注:Intel64架构以前称为Extended Memory 64 Technology,对应的编译器选项是/favor:EM64T。
(1) 打开项目的属性页对话框。细节请参见在Visual Studio中设置C++编译和构建属性文档部分。
(2) 选择配置属性-> C/C++ -> 命令行 属性页。
(3) 在附加选项框中输入编译选项。
本节描述X64架构各种数据类型及相对应的存储。
尽管可以以任何对齐方式访问数据,但是为了避免性能损失,我们仍推荐在内存的自然边界或多个自然边界上对齐访问数据。枚举是一个常量整数,按32位整数对待。下表描述了数据的类型定义和建议的存储,因为它适合使用以下值对齐:
字节(Byte)——8位
字(Word)——16位
双字(Doubleword)——32位
四字(Quadword)——64位
八字(Octadword)——128位
标量类型 |
C语据类型 |
存储大小(按字节) |
推荐对齐 |
INT8(8位整数,下同) |
char |
1 |
字节 |
UINT8 |
unsigned char |
1 |
字节 |
INT16 |
short |
2 |
字 |
UINT16 |
unsigned short |
2 |
字 |
INT32 |
int,long (注:在64位环境下,windows都占4个字节;但linux下int占4字节,long占8字节) |
4(注:linux平台下long占8字节) |
双字(注:若long占8个字节,对应方式应为四字) |
UINT32 |
nt,long (注:在64位环境下,windows都占4个字节;但linux下unsinged int占4字节,unsigned long占8字节) |
4(注:linux平台下unsigned long占8字节) |
双字(注:若unsigned long占8个字节,对应方式应为四字) |
INT64 |
__int64(linux下自定义为long long) |
8 |
四字 |
UINT64 |
Unsigned __int64(linux下自定义为unsigned long long) |
8 |
四字 |
FP32(浮点精度) |
float |
4 |
双字 |
FP64(浮点精度) |
double |
8 |
四字 |
POINTER |
* |
8 |
四字 |
__m64 |
struct __m64 |
8 |
四字 |
__m128 |
struct __m128 |
16 |
八字 |
其它数据类型,例如,数组、结构、联合,都有严格的对齐要求,以确保聚合类型和联合类型以地址连续的方式存取。下面是数组、结构、联合的定义:
包含一组有序的相邻数据对象。每个对象称为一个元素。数组内的所有元素都具有相同的大小和数据类型。
包含一组有序的数据对象。与数组不同的是,结构内的数据对象可以有不同的数据类型和大小。每个结构内的对象称为一个成员。
包含一组命名成员中的任何一个的对象。这个命名集合的成员可以是任何类型。系统为联合类型分配的存储空间等于存储联合的最大成员所要求的空间大小,再加上按对齐要求所需的填充空间的大小。
下表展示了对联合和结构这种标量类型存储所强烈建议的对齐字节数:
标量类型 |
C语据类型 |
存储大小(按字节) |
要求对齐 |
INT8(8位整数,下同) |
char |
1 |
字节 |
UINT8 |
unsigned char |
1 |
字节 |
INT16 |
short |
2 |
字 |
UINT16 |
unsigned short |
2 |
字 |
INT32 |
int,long (注:在64位环境下,windows都占4个字节;但linux下int占4字节,long占8字节) |
4(注:linux平台下long占8字节) |
双字(注:若long占8个字节,对应方式应为四字) |
UINT32 |
nt,long (注:在64位环境下,windows都占4个字节;但linux下unsinged int占4字节,unsigned long占8字节) |
4(注:linux平台下unsigned long占8字节) |
双字(注:若unsigned long占8个字节,对应方式应为四字) |
INT64 |
__int64(linux下自定义为long long) |
8 |
四字 |
UINT64 |
Unsigned __int64(linux下自定义为unsigned long long) |
8 |
四字 |
FP32(浮点精度) |
float |
4 |
双字 |
FP64(浮点精度) |
double |
8 |
四字 |
POINTER |
* |
8 |
四字 |
__m64 |
struct __m64 |
8 |
四字 |
__m128 |
struct __m128 |
16 |
八字 |
下面的聚合类型适用的对齐规则:
(1) 一个数组的对齐与一个数组的元素中的某一个元素的对齐相同。
(2) 结构或联合开头的对齐方式是任何单个成员的最大对齐方式。结构或联合中的每一个成员必须放置在前面列表中定义的合适的对齐位置,对齐规则可能会要求隐式的内部填充,这取决于前一个成员的大小。
(3) 结构体的大小必须是其对齐字节的整数倍大小,系统可能会要求在其最后的成员之后对不足长度的部分进行填充。由于结构和联合可以分组到数组中,因此结构或联合的每个数组元素必须以先前确定的正确对齐方式开始和结束。
(4) 只要按照先前确定的对齐规则,以大于对齐所需的最小存储空间存储数据是有可能的(注:也就是说,以恰好的大小对齐内存边界,不浪费内存,即没有填充字节,这是理想的情况;出现所需空间大于这个理想空间是可能的,因为定义成员的顺序不一样,按规则所需的空间大小也不一样)。
(5) 单个编译器可能会出于大小原因调整结构的打包。例如/Zp(结构成员对齐)允许调整结构的打包。
(1) 示例1
// 总大小 = 2 字节, 对齐字节= 2 字节(word).
_declspec(align(2)) struct {
short a; // +0; size = 2 字节(+0,表示其上一个元素的位置,下同)
}
示意图:
(2) 示例2
// 总大小 = 24 字节, 对齐字节= 8 字节(quadword).
_declspec(align(8)) struct {
int a; // +0; size = 4 字节
double b; // +8; size = 8 字节(前面a不够8字节,用4个字节填充,+8开始)
short c; // +16; size = 2 字节
}
示意图:
3) 示例3
// 总大小 = 12 字节, 对齐字节= 4 字节(doubleword).
_declspec(align(4)) struct {
char a; // +0; size = 1 字节
short b; // +2; size = 2字节
char c; // +4; size = 1字节
int d; // +8; size = 4字节
}
示意图:
(4) 示例4
// 总大小 = 8 字节, 对齐字节= 8 字节(quadword).
_declspec(align(8)) union {
char *p; // +0; size = 8字节 (指针当整数对待,64位平台指针点8字节)
short s; // +0; size = 2字节
long l; // +0; size = 4字节 (linux平台下为8)
}
示意图:
结构位域限定在64位之内,可以是类型signed int,unsigned int,int64,或unsigned int64。跨越类型边界的位域将跨过边界位将位对齐到下一个类型的对齐边界。例如,整数位域不能跨越32位的边界。
当你使用X86编译器编译一个应用程序的时候,大于4字节的数据类型不会自动地对齐于栈的边界。因为X86编译器是一个4字节的对齐栈,任何大于4字节的数据类型,例如64位整数,不会自动地对齐于8字节的地址。
运行带有非对齐数据的程序有两方面的影响:
(1) 相对于访问对齐数据的内存地址,访问非对齐数据的内存地址会消耗更长的时间。
(2) 非对齐数据的内存地址不能用于自旋锁的操作(interlocked operations)。
如果你要求严格的数据对齐,可以在声明变量时使用声明符号__declspec(align(N))。这会促使编译器动态对齐栈以满足你的对齐规范要求。然而,在程序运行时动态调整栈对齐可能会拖慢程序的运行速度。
X64架构提供了16个通用寄存器(以下简称整型寄存器)以及16个XMM/YMM浮点寄存器。易失性寄存器(volatile registers)是调用者认为在调用函数过程中会被销毁的暂存寄存器(scratch registers)。非易失性寄存器(nonvolatile registers)要求在调用函数期间保留其值,并且,如果被调函数修改了其值,也必须事先保存下来,完成后将其恢复。
下面的表描述了在函数调用的时候如何使用每个寄存器——寄存器易失性和保存:
寄存器 |
状态 |
用法 |
RAX |
Volatile |
返回值值存器 |
RCX |
Volatile |
第1个整型参数 |
RDX |
Volatile |
第2个整型参数 |
R8 |
Volatile |
第3个整型参数 |
R9 |
Volatile |
第4个整型参数 |
R10:R11 |
Volatile |
必须由调用函数根据需要保存; 在 syscall/sysret 指令中使用 |
R12:R15 |
Nonvolatile |
必须由被调函数保存 |
RDI |
Nonvolatile |
必须由被调函数保存 |
RSI |
Nonvolatile |
必须由被调函数保存 |
RBX |
Nonvolatile |
必须由被调函数保存 |
RBP |
Nonvolatile |
可作为栈帧指针; 必须由被调函数保存 |
RSP |
Nonvolatile |
栈指针 |
XMM0, YMM0 |
Volatile |
第1个浮点数参数;当使用__vectorcall 时用于传递第1个向量类型参数 |
XMM1, YMM1 |
Volatile |
第2个浮点数参数;当使用__vectorcall 时用于传递第2个向量类型参数 |
XMM2, YMM2 |
Volatile |
第3个浮点数参数;当使用__vectorcall 时用于传递第3个向量类型参数 |
XMM3, YMM3 |
Volatile |
第4个浮点数参数;当使用__vectorcall 时用于传递第4个向量类型参数 |
XMM4, YMM4 |
Volatile |
必须由被调函数保存; 当使用__vectorcall 时用于传递第5个向量类型参数 |
XMM5, YMM5 |
Volatile |
必须由调用函数保存; 当使用__vectorcall 时用于传递第6个向量类型参数 |
XMM6:XMM15, YMM6:YMM15 |
Nonvolatile (XMM), Volatile (upper half of YMM) |
必须由被调函数保存。 YMM寄存器必须由调用函数根据需要保存。 |
在函数退出和函数进入 C 运行时库调用和 Windows 系统调用时,预期会清除掉CPU 标志寄存器中的方向标志位。
所有RSP的当前地址之外的内存都被认为是易失性的:操作系统或者调试器可能会在用户调试会话期间、或者中断处理期间,重写这些内存。因此,在试图使用RSP读取数据或将数据写入栈帧之前,总是必须先设置RSP,使之指向一块有效的栈帧地址。下面讨论用于局部变量的栈空间的分配内置alloca函数(alloca intrinsic)。
一个函数的序言(prolog)对为局部变量、寄存器值、栈参数、以及寄存器参数的临时存储而分配栈空间的这种行为负责。
参数区域总是位于栈底(即使调用alloca函数也是如此),因此,在调用任何函数期间,它总是比邻函数调用完成后的返回地址。它至少包括4个条目(实际上还包括返回地址),但实践中总是为任何可能调用的函数传递需要的参数分配足够的空间用于存储所有的参数。需要指出的是,即使函数参数永远不会位于栈的开头(即栈底),系统也始终会为寄存器参数的传递分配空间(甚至0个寄存器,这块栈空间仍然会被保留,即所谓“影子内存”);系统一定会确保被调函数参数所需的所有空间都被分配到位。要求寄存器参数使用起始址,是因为可以获得一块连续的区域,以便被调函数需要参数列表(va_list)或者某个参数的地址时可以提供。这块区域也为形实转换(thunk execution)期间存储寄存器参数提供了一个便利的存储位置,并且作为一个调试选项(例如,假如将参数存储在序言代码的起始位置,它就使得在调试的时候,很容易找到参数)。即使被调函数的参数少于4个,这4个栈的位置也属于被调函数,除了存储寄存器参数值,也可以被被调函数用于其它目的。因此,调用函数在调用别的函数期间,不可以在这块栈空间中存储信息(影子内存)。
假如空间是动态分配(例如,使用函数alloca分配),则必须使用非易失性寄存器作为帧指针来标记堆栈固定部分的基址,并且必须在prolog中保存和初始化该寄存器。需指出,当使用alloca的时候,在内一个调用函数中调用同一个被调函数,可以使用不同的栈首地址传递它们的参数。
栈空间总是保持16字节边界对齐,prolog例外(例如,在返回地址之后入栈),以及用于某些帧函数类型标明函数类型(Function Types)的场合例外。
下图是一个函数A调用一个非叶函数B (a non-leaf function B)的栈分布示例。函数A的prolog已经为所有寄存器分配了空间。B要求的栈参数位于栈底。调用函数压入返回地址到栈上,而B的prolog为其局部变量、非易失性寄存器、以及其进一步地调用其它函数分配存储空间。假如B使用了alloca,则分配的空间位于局部变量/非易失性寄存器存储区域和参数栈区域之间。如下图所示:
当B进一步调用另一个函数的时候,其返回地址被压入RCX起始地址的正下方。
假如使用了栈指针,则存在动态创建参数堆栈区域的选项。目前在X64 编译器中没有这样做。
存在两种基本的函数类型。要求栈帧的函数称为帧函数(frame functions),和不要求栈帧的函数称为叶函数(leaf functions,叶子节点,没有更下面的子叶了)。
帧函数分配栈空间、调用其它函数、存储非易失性寄存器的值、或者使用异常处理。也要求一个函数表条目(a function table entry)。帧函数要求prolog和epilog。帧函数可以动态分配栈空间,并且可以使用一个帧指针。一个帧函数具备这个调用标准在其处理上的全部能力。
假如一个帧函数没有调用另一个函数,则其不要求对齐栈空间(参见栈分配的文档)。
叶函数不要求函数表条目。它不会改变任何非易失性寄存器的值,包括RSP在内,意味着它不会调用任何函数,也不会分配任何栈空间(注:即,叶函数名称的特点所在)。当执行的时候,允许它在没对齐的情况下离开栈空间。
叶函数可以确保返回合适的对齐内存,以便于存储任何这样的对象——它们要求满足基本内存对齐要求,并且系统又可以为这样的对象分配合适的内存(也就是说,如果内存要求过多,则系统不能满足)。基本内存对齐是指,在没有对齐规范的情况下,小于或者等于系统实现所支持的最大对齐字节数。(在VC++要求按double类型或者按8字节对齐。在面向64位平台的代码中,要求按照16字节对齐。) 例如,4字节的内存分配会对齐在一个支持任何4字节或者更小字节对象的边界对齐(注:实际上,小于4字节的数据,也分配4字节的存储空间,不足部分用字节填充)。
C++允许扩展对齐(extended alignment)类型(也称为溢出对齐(over-alignment)类型)。例如,SSE的_m128和_m256类型,以及使用__declspec(align( n ))符号修饰的类型(其中n大于8,具有扩展类型)。malloc不保证适合要求使用扩展对齐的对象在内存边界对齐。要为益出类型分配内存,请使用_aligned_malloc及其相关函数。
_alloca函数要求16字节对齐且还要求使用帧指针。
分配的栈需要在其后面包含用于后续调用函数的参数的空间,正如6.1栈分配部分所述。
分配栈空间、调用其它的函数、保存非易失性寄存器、或者使用异常处理的的每一个函数,都必须要有一个prolog(这个prolog的地址限制在与相应函数表条目相关的unwind数据(解开数据)中描述)和在每次在退出函数时都必须要有一个epilog。关于在X64平台所要求的prolog和epilog代码,请求见X64 prolog和epilog说明文档。
有关用于在X64上实现结构化异常处理和C++异常处理行为的约定和数据结构的信息,请参阅 X64 异常处理文档。
X64编译器的限制之一是不支持内联汇编程序。这意味着不能用C或C++编写的函数要么必须编写为子例程,要么编写为编译器支持的内置函数。某些功能对性能敏感,而其他功能则不是。性能敏感函数应该作为内置函数来实现。
编译器支持的内置函数请参见编译器内置函描述文档。
X64可执行映像是PE32+格式。可执行映像(包括DLL和EXE)严格限定在2G大小以内,因此,带有32位位移的相对地址可以用于静态映像数据的寻址。这些数据包括导入地址表(import address table)、字符常量、静态全局数据、等等。
资料来源:
x64 ABI conventions | Microsoft DocsLearn more about: x64 ABI conventionshttps://docs.microsoft.com/en-us/cpp/build/x64-software-conventions?view=msvc-170