本主题介绍 x64(x86 体系结构的 64 位扩展)的基本应用程序二进制接口 (ABI)。 其中包含调用约定、类型布局、堆栈和寄存器使用情况等主题。
x64 调用约定
x86 和 x64 之间的两个重要差异是:
64 位寻址功能
常规用途的十六个 64 位寄存器。
给定扩展寄存器集,x64 使用 __fastcall 调用约定和基于 RISC 的异常处理模型。
__fastcall 约定对前四个参数和堆栈帧使用寄存器来传递更多参数。 有关 x64 调用约定的详细信息,包括寄存器使用情况、堆栈参数、返回值和堆栈展开,请参阅 x64 调用约定。
有关 __vectorcall 调用约定的详细信息,请参阅 __vectorcall。
启用 x64 编译器优化
以下编译器选项有助于优化 x64 应用程序:
/favor(优化体系结构详细信息)
x64 类型和存储布局
本部分介绍 x64 体系结构的数据类型的存储。
标量类型
尽管可以采用任意对齐方式访问数据,但为了避免性能损失,建议在其自然边界或自然边界的倍数上对齐数据。 枚举是常量整数,并视为 32 位整数。 下表描述了类型定义和建议的数据存储,因为它与使用以下对齐值的对齐有关:
字节 - 8 位
字 - 16 位
双字 - 32 位
四字 - 64 位
八倍长字 - 128 位
标量类型
C 数据类型
存储大小(以字节为单位)
建议的对齐方式
INT8
char
1
字节
UINT8
unsigned char
1
字节
INT16
short
2
单词
UINT16
unsigned short
2
单词
INT32
int、long
4
双字
UINT32
unsigned int、unsigned long
4
双字
INT64
__int64
8
四字
UINT64
unsigned __int64
8
四字
FP32(单精度)
float
4
双字
FP64(双精度)
double
8
四字
POINTER
*
8
四字
__m64
struct __m64
8
四字
__m128
struct __m128
16
八倍长字
x64 聚合和联合布局
其他类型(如 arrays、struct 和 union)具有更严格的对齐要求,可确保一致的聚合和联合存储和数据检索。 下面是数组、结构和联合的定义:
数组
包含相邻数据对象的有序组。 每个对象称为一个“元素”。 数组中的所有元素都具有相同的大小和数据类型。
结构
包含数据对象的有序组。 与数组的元素不同,结构的成员可以有不同的数据类型和大小。
联盟
包含一组已命名成员的任一成员的对象。 已命名组成员可以是任何类型。 为联合分配的存储等于该联合的最大成员所需的存储,外加对齐所需的任何填充字节。
下表显示了对联合和结构的标量成员强烈建议的对齐方式。
标量类型
C 数据类型
所需的对齐方式
INT8
char
字节
UINT8
unsigned char
字节
INT16
short
单词
UINT16
unsigned short
单词
INT32
int、long
双字
UINT32
unsigned int、unsigned long
双字
INT64
__int64
四字
UINT64
unsigned __int64
四字
FP32(单精度)
float
双字
FP64(双精度)
double
四字
POINTER
*
四字
__m64
struct __m64
四字
__m128
struct __m128
八倍长字
以下聚合对齐规则适用于:
数组的对齐方式与数组的一个元素的对齐方式相同。
结构或联合的开头对齐方式是任何单个成员的最大对齐方式。 结构或联合中的每个成员必须置于上表中定义的正确对齐方式,这可能需要隐式内部填充,具体取决于前一个成员。
结构大小必须是其对齐的整数倍,这可能需要在最后一个成员之后填充字节。 由于结构和联合可以分组在数组中,因此结构或联合的每个数组元素都必须在先前确定的正确对齐方式下开始和结束。
只要保留以前的规则,就可以以大于对齐要求的方式对齐数据。
单个编译器可能会出于大小原因调整结构的包装。 例如 /Zp(结构成员对齐)允许调整结构的封装。
x64 结构对齐示例
以下四个示例分别声明对齐的结构或联合,相应的数字说明了该结构或内存中的联合的布局。 图中的每列表示内存的字节,列中的数字表示该字节的位移。 每个图形的第二行中的名称对应于声明中变量的名称。 有阴影的列表示实现指定对齐所需的填充。
示例 1
// Total size = 2 bytes, alignment = 2 bytes (word).
_declspec(align(2)) struct {
short a; // +0; size = 2 bytes
}
示例 2
// Total size = 24 bytes, alignment = 8 bytes (quadword).
_declspec(align(8)) struct {
int a; // +0; size = 4 bytes
double b; // +8; size = 8 bytes
short c; // +16; size = 2 bytes
}
此图显示了 24 字节的内存。 成员 a,int,占用字节 0 到 3。 此图显示了字节 4 到 7 的填充。 成员 b(双精度)占用字节 8 到 15。 成员 c(短)占用字节 16 到 17。 未使用字节 18 到 23。
示例 3
// Total size = 12 bytes, alignment = 4 bytes (doubleword).
_declspec(align(4)) struct {
char a; // +0; size = 1 byte
short b; // +2; size = 2 bytes
char c; // +4; size = 1 byte
int d; // +8; size = 4 bytes
}
此图显示了 12 个字节的内存。 成员 a 是一个字符,占用第 0 字节。 字节 1 将被填充。 成员 b(短)占用字节 2 到 4。 字符成员 c 占用字节 4。 字节 5 到 7 是填充字节。 成员 d(int)占用字节 8 到 11。
示例 4
// Total size = 8 bytes, alignment = 8 bytes (quadword).
_declspec(align(8)) union {
char *p; // +0; size = 8 bytes
short s; // +0; size = 2 bytes
long l; // +0; size = 4 bytes
}
此图显示了 8 个字节的内存。 成员 p,字符,占用字节 0。 成员 s(短整型)占用字节 0 到 1。 成员 l,类型为 long,占用字节 0 到 3。 字节 4 到 7 是填充字节。
位域
结构位字段限制为 64 位,可以是类型符号 int、无符号 int、int64 或无符号 int64。 跨越类型边界的位字段将跳过位以将位字段与下一个类型对齐方式对齐。 例如,整数位域不能跨 32 位边界。
与 x86 编译器冲突
使用 x86 编译器编译应用程序时,大于 4 个字节的数据类型不会在堆栈上自动对齐。 由于 x86 编译器的体系结构是 4 字节对齐堆栈,因此任何大于 4 字节(例如 64 位整数)都不能自动与 8 字节地址对齐。
使用未对齐的数据有两个含义。
访问未对齐位置所需的时间可能比访问对齐位置所需的时间长。
未对齐的位置不能用于互锁操作。
如果需要更严格的对齐方式,请使用变量声明上的 __declspec(align(N))。 这将导致编译器动态对齐堆栈以满足你的规范。 但是,在运行时动态调整堆栈可能会导致应用程序执行速度变慢。
x64 寄存器使用情况
x64 体系结构提供了 16 个通用寄存器(以后称为整数寄存器),以及 16 个可供浮点使用的 XMM/YMM 寄存器。 易失寄存器是由调用方假想的临时寄存器,并要在调用过程中销毁。 非易失寄存器需要在整个函数调用过程中保留其值,并且一旦使用,则必须由被调用方保存。
寄存器的易失性和保存方式
下表说明了每种寄存器在整个函数调用过程中的使用方法:
注册
状态
用途
RAX
不稳定的
返回值寄存器
RCX
不稳定的
第一个整型自变量
RDX
不稳定的
第二个整型自变量
R8
不稳定的
第三个整型自变量
R9
不稳定的
第四个整型自变量
R10:R11
不稳定的
必须根据需要由调用方保留;在 syscall/sysret 指令中使用
R12:R15
非易失的
必须由被调用方保留
RDI
非易失的
必须由被调用方保留
RSI
非易失的
必须由被调用方保留
RBX
非易失的
必须由被调用方保留
RBP
非易失的
可用作帧指针;必须由被调用方保留
RSP
非易失的
堆栈指针
XMM0、YMM0
不稳定的
第一个 FP 参数;使用 __vectorcall 时的第一个矢量类型参数
XMM1、YMM1
不稳定的
第二个 FP 参数;使用 __vectorcall 时的第二个矢量类型参数
XMM2、YMM2
不稳定的
第三个 FP 参数;使用 __vectorcall 时的第三个矢量类型参数
XMM3、YMM3
不稳定的
第四个 FP 参数;使用 __vectorcall 时的第四个矢量类型参数
XMM4、YMM4
不稳定的
必须根据调用方的需要保留;使用 __vectorcall 时的第五个矢量类型参数
XMM5、YMM5
不稳定的
必须根据调用方的需要保留;使用 __vectorcall 时的第六个矢量类型参数
XMM6:XMM15、YMM6:YMM15
非易失性(XMM),易失性(YMM 的上半部分)
必须由被调用方保留。 YMM 寄存器必须根据需要由调用方保留。
当函数进入和退出 C 运行时库调用和 Windows 系统调用时,CPU 标志寄存器的方向位标志将被清除。
堆栈使用
有关 x64 上的堆栈分配、对齐、函数类型和堆栈帧的详细信息,请参阅 x64 堆栈使用情况。
Prolog 和 Epilog
分配堆栈空间、调用其他函数、保存非易失性寄存器或使用异常处理的每个函数都必须具有 prolog,其地址限制在与相应函数表条目和每个函数出口上的 epilog 关联的展开数据中进行了描述。 有关 x64 上所需的 prolog 和 epilog 代码的详细信息,请参阅 x64 prolog 和 epilog。
x64 异常处理
有关用于在 x64 上实现结构化异常处理和 C++ 异常处理行为的约定和数据结构的信息,请参阅 x64 异常处理。
内部函数和内联程序集
x64 编译器的约束之一是没有内联汇编器支持。 这意味着不能用 C 或 C++ 编写的函数必须编写为子例程,或必须编写为编译器支持的内部函数。 某些函数对性能敏感,而其他函数则不敏感。 对性能敏感的函数应作为内部函数实现。
有关编译器支持的内部函数,请参阅编译器内部函数。
x64 图像格式
x64 可执行映像的格式为 PE32 +。 可执行映像(DLL 和 EXE)的最大大小限制为 2 GB,因此具有 32 位偏移的相对寻址可用于处理静态映像数据。 此数据包括导入地址表、字符串常量、静态全局数据等。
另请参阅
调用约定