x64 ABI 约定概述

本主题介绍 x64(x86 体系结构的 64 位扩展)的基本应用程序二进制接口 (ABI)。 其中包含调用约定、类型布局、堆栈和寄存器使用情况等主题。

x64 调用约定

x86 和 x64 之间的两个重要差异是:

  • 64 位寻址功能
  • 常规用途的十六个 64 位寄存器。

给定扩展的寄存器组 x64 使用 __fastcall 调用约定和基于 RISC 的异常处理模型。

__fastcall 约定对前四个参数和堆栈帧使用寄存器来传递更多参数。 有关 x64 调用约定的详细信息,包括寄存器使用情况、堆栈参数、返回值和堆栈展开,请参阅 x64 调用约定

有关 __vectorcall 调用约定的详细信息,请参阅 __vectorcall

启用 x64 编译器优化

以下编译器选项有助于优化 x64 应用程序:

x64 类型和存储布局

本部分介绍 x64 体系结构的数据类型的存储。

标量类型

尽管可以以任何对齐方式访问数据,但请在其自然边界或自然边界倍数上对齐数据,以避免性能损失。 枚举是常量整数,并视为 32 位整数。 下表描述了类型定义和建议的数据存储,因为它与使用以下对齐值的对齐有关:

  • 字节 - 8 位
  • 字 - 16 位
  • 双字 - 32 位
  • 四字 - 64 位
  • 八倍长字 - 128 位
标量类型 C 数据类型 存储大小(以字节为单位) 建议的对齐方式
INT8 char 1 Byte
UINT8 unsigned char 1 Byte
INT16 short 2 Word
UINT16 unsigned short 2 Word
INT32 intlong 4 双字
UINT32 unsigned intunsigned 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)具有更严格的对齐要求,可确保一致的聚合和联合存储和数据检索。 下面是数组、结构和联合的定义:

  • 数组

    包含相邻数据对象的有序组。 每个对象称为一个“元素”。 数组中的所有元素都具有相同的大小和数据类型。

  • 结构

    包含数据对象的有序组。 与数组的元素不同,结构的成员可以有不同的数据类型和大小。

  • Union

    包含一组已命名成员的任一成员的对象。 已命名组成员可以是任何类型。 为联合分配的存储等于该联合的最大成员所需的存储,外加对齐所需的任何填充字节。

下表显示了对联合和结构的标量成员强烈建议的对齐方式。

标量类型 C 数据类型 所需的对齐方式
INT8 char Byte
UINT8 unsigned char Byte
INT16 short Word
UINT16 unsigned short Word
INT32 intlong 双字
UINT32 unsigned intunsigned 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
}

显示示例 1 结构布局的关系图。

示例 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
}

显示示例 2 结构布局的关系图。

示例 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
}

显示示例 3 结构布局的关系图。

示例 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
}

显示示例 4 联合布局的关系图。

位域

结构位字段限制为 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 位偏移的相对寻址可用于处理静态映像数据。 此数据包括导入地址表、字符串常量、静态全局数据等。

另请参阅

调用约定