ARM64EC ABI 约定概述

ARM64EC 是一个应用程序二进制接口 (ABI),它使 ARM64 二进制文件能够本机运行,并与 x64 代码互操作。 具体而言,ARM64EC ABI 遵循 x64 软件约定,包括调用约定、堆栈用法和数据对齐,使 ARM64EC 和 x64 代码可以互操作。 操作系统模拟二进制文件的 x64 部分。 (ARM64EC 中的 EC 代表仿真兼容。)

有关 x64 和 ARM64 ABI 的详细信息,请参阅 x64 ABI 约定概述ARM64 ABI 约定概述

ARM64EC 并不能解决基于 x64 和 ARM 的体系结构之间的内存模型差异。 有关详细信息,请参阅常见的 Visual C++ ARM 迁移问题

定义

  • ARM64 - 包含传统 ARM64 代码的 ARM64 进程的代码流。
  • ARM64EC - 利用 ARM64 寄存器集的子集提供与 x64 代码的互操作性的代码流。

寄存器映射

x64 进程可能具有运行 ARM64EC 代码的线程。 因此,始终可以检索 x64 寄存器上下文,ARM64EC 使用 ARM64 核心寄存器的子集 1:1 映射到模拟的 x64 寄存器。 重要的是,除了从 x18 读取线程环境块 (TEB) 地址之外,ARM64EC 从不使用此子集之外的寄存器。

当某些或许多函数重新编译为 ARM64EC 时,本机 ARM64 进程的性能不应下降。 为了保持性能,ABI 遵循以下原则:

  • ARM64EC 寄存器子集包括所有属于 ARM64 函数调用约定的寄存器。

  • ARM64EC 调用约定直接映射到 ARM64 调用约定。

特殊帮助程序例程(如 __chkstk_arm64ec)使用自定义调用约定和寄存器。 这些寄存器也包含在 ARM64EC 寄存器子集中。

整数寄存器的寄存器映射

ARM64EC 寄存器 x64 寄存器 ARM64EC 调用约定 ARM64 调用约定 x64 调用约定
x0 rcx volatile volatile volatile
x1 rdx volatile volatile volatile
x2 r8 volatile volatile volatile
x3 r9 volatile volatile volatile
x4 r10 volatile volatile volatile
x5 r11 volatile volatile volatile
x6 mm1(x87 R1 寄存器的低 64 位) volatile volatile volatile
x7 mm2(x87 R2 寄存器的低 64 位) volatile volatile volatile
x8 rax volatile volatile volatile
x9 mm3(x87 R3 寄存器的低 64 位) volatile volatile volatile
x10 mm4(x87 R4 寄存器的低 64 位) volatile volatile volatile
x11 mm5(x87 R5 寄存器的低 64 位) volatile volatile volatile
x12 mm6(x87 R6 寄存器的低 64 位) volatile volatile volatile
x13 不可用 不允许 volatile 空值
x14 空值 不允许 volatile 不可用
x15 mm7(x87 R7 寄存器的低 64 位) volatile volatile volatile
x16 每个 x87 R0-R3 寄存器的高 16 位 易失性 (xip0) 易失性 (xip0) volatile
x17 每个 x87 R4-R7 寄存器的高 16 位 易失性 (xip1) 易失性 (xip1) volatile
x18 GS.base 固定 (TEB) 固定 (TEB) 固定 (TEB)
x19 r12 非易失性 非易失性 非易失性
x20 r13 非易失性 非易失性 非易失性
x21 r14 非易失性 非易失性 非易失性
x22 r15 非易失性 非易失性 非易失性
x23 不可用 不允许 非易失性 空值
x24 空值 不允许 非易失性 不可用
x25 rsi 非易失性 非易失性 非易失性
x26 rdi 非易失性 非易失性 非易失性
x27 rbx 非易失性 非易失性 非易失性
x28 不可用 不允许 不允许 不可用
fp rbp 非易失性 非易失性 非易失性
lr mm0(x87 R0 寄存器的低 64 位) Azure 和 AppSource Azure 和 AppSource Azure 和 AppSource
sp rsp 非易失性 非易失性 非易失性
pc rip 指令指针 指令指针 指令指针
PSTATE 子集:N/Z/C/V/SS1、2 RFLAGS 子集:SF/ZF/CF/OF/TF volatile volatile volatile
不可用 RFLAGS 子集:PF/AF 空值 空值 volatile
不可用 RFLAGS 子集:DF 空值 空值 非易失性

1 避免直接读取、写入或计算 PSTATERFLAGS 之间的映射。 这些位可能在将来使用,并且可能会发生更改。

2 ARM64EC 携带标志 C 是 x64 携带标志 CF 的反转,用于减法运算。 没有特殊处理,因为标志是易失的,因此在(ARM64EC 和 x64)函数之间转换时会进行回收。

向量寄存器的寄存器映射

ARM64EC 寄存器 x64 寄存器 ARM64EC 调用约定 ARM64 调用约定 x64 调用约定
v0-v5 xmm0-xmm5 volatile volatile volatile
v6-v7 xmm6-xmm7 volatile volatile 非易失性
v8-v15 xmm8-xmm15 易失性 1 易失性 1 非易失性
v16-v31 xmm16-xmm31 不允许 volatile disallowed(x64 仿真器不支持 AVX-512)
FPCR2 MXCSR[15:6] 非易失性 非易失性 非易失性
FPSR2 MXCSR[5:0] volatile volatile volatile

1 这些 ARM64 寄存器的特殊之处在于低 64 位是非易失性的,而高 64 位是易失性的。 从 x64 调用方的角度来看,它们实际上是易失性的,因为被调用方会回收数据。

2 避免直接读取、写入或计算 FPCRFPSR 的映射。 这些位可能在将来使用,并且可能会发生更改。

结构打包

ARM64EC 遵循用于 x64 的相同结构打包规则,以确保 ARM64EC 代码和 x64 代码之间的互操作性。 有关 x64 结构打包的详细信息和示例,请参阅 x64 ABI 约定概述

仿真帮助程序 ABI 例程

ARM64EC 代码和 thunk 使用仿真帮助程序例程在 x64 和 ARM64EC 函数之间转换。

下表描述了每个特殊的 ABI 例程和 ABI 使用的寄存器。 例程不会修改 ABI 列下列出的保留寄存器。 不应对未列出的寄存器做出任何假设。 在磁盘上,ABI 例程指针为 null。 在加载时,加载程序会更新指向 x64 仿真器例程的指针。

名称 描述 ABI
__os_arm64x_dispatch_call_no_redirect 由出口 thunk 调用,以调用 x64 目标(x64 函数或 x64 快进序列)。 该例程推送 ARM64EC 返回地址(在 LR 寄存器中),然后推送调用 x64 仿真器的 blr x16 指令之后的指令地址。 然后,它运行 blr x16 指令 x8 (rax) 中的返回值
__os_arm64x_dispatch_ret 由入口 thunk 调用以返回到其 x64 调用方。 它从堆栈中弹出 x64 返回地址,并调用 x64 仿真器跳转到它 不可用
__os_arm64x_check_call 由 ARM64EC 代码调用,其中包含一个指向出口 thunk 和要执行的间接 ARM64EC 目标地址的指针。 ARM64EC 目标被认为是可修补的,执行始终返回给调用方:要么是调用它时使用的相同数据,要么是修改后的数据 参数:
x9:目标地址
x10:出口 thunk 地址
x11:快进序列地址

Out:
x9:如果目标函数被绕过,则它包含快进序列的地址
x10:出口 thunk 地址
x11:如果函数被绕过,则它包含出口 thunk 地址。 否则,目标地址跳转到

保留的寄存器:x0-x8x15 (chkstk)。 和 q0-q7
__os_arm64x_check_icall 通过 ARM64EC 代码(指向出口 thunk 的指针)调用,以处理到 x64 或 ARM64EC 的目标地址的跳转。 如果目标为 x64 且尚未修补 x64 代码,则例程设置目标地址寄存器。 它指向函数的 ARM64EC 版本(如果存在)。 否则,它将寄存器设置为指向转换为 x64 目标的出口 thunk。 然后,它将返回到调用 ARM64EC 代码,然后跳转到寄存器中的地址。 此例程是未优化版本的 __os_arm64x_check_call,其中目标地址在编译时是未知的

在间接调用的调用站点上使用
参数:
x9:目标地址
x10:出口 thunk 地址
x11:快进序列地址

Out:
x9:如果目标函数被绕过,则它包含快进序列的地址
x10:出口 thunk 地址
x11:如果函数被绕过,则它包含出口 thunk 地址。 否则,目标地址跳转到

保留的寄存器:x0-x8x15 (chkstk) 和 q0-q7
__os_arm64x_check_icall_cfg __os_arm64x_check_icall 相同,但还检查指定的地址是否为有效的控制流图间接调用目标 参数:
x10:出口 thunk 的地址
x11:目标函数的地址

Out:
x9:如果目标为 x64,则为函数的地址。 其他情况下则不定义
x10:出口 thunk 的地址
x11:如果目标为 x64,则它包含出口 thunk 的地址。 否则,为函数的地址

保留的寄存器:x0-x8x15 (chkstk) 和 q0-q7
__os_arm64x_get_x64_information 获取实时 x64 寄存器上下文的请求部分 _Function_class_(ARM64X_GET_X64_INFORMATION) NTSTATUS LdrpGetX64Information(_In_ ULONG Type, _Out_ PVOID Output, _In_ PVOID ExtraInfo)
__os_arm64x_set_x64_information 设置实时 x64 寄存器上下文的请求部分 _Function_class_(ARM64X_SET_X64_INFORMATION) NTSTATUS LdrpSetX64Information(_In_ ULONG Type,_In_ PVOID Input, _In_ PVOID ExtraInfo)
__os_arm64x_x64_jump 用于无签名调整器和其他直接将调用转发 (jmp) 到另一个可以具有任何签名的函数的 thunk,从而将正确的 thunk 的潜在应用推迟到实际目标 参数:
x9:要跳转到的目标

保留(转发)的所有参数寄存器

thunk

thunk 是支持 ARM64EC 和 x64 函数相互调用的低级别机制。 有两种类型:入口 thunk(用于输入 ARM64EC 函数),以及出口 thunk(用于调用 x64 函数)

入口 thunk 和内部入口 thunk:x64 到 ARM64EC 函数的调用

为了在 C/C++ 函数编译为 ARM64EC 时支持 x64 调用方,工具链会生成一个由 ARM64EC 计算机代码组成的单一入口 thunk。 内部函数有一个自己的入口 thunk。 所有其他函数与具有匹配的调用约定、参数和返回类型的所有函数共享一个入口 thunk。 thunk 的内容取决于 C/C++ 函数的调用约定。

除了处理参数和返回地址之外,thunk 还弥补了由 ARM64EC 向量寄存器映射引起的 ARM64EC 和 x64 向量寄存器之间的波动性差异:

ARM64EC 寄存器 x64 寄存器 ARM64EC 调用约定 ARM64 调用约定 x64 调用约定
v6-v15 xmm6-xmm15 易失性,但在入口 thunk 中保存/恢复(x64 到 ARM64EC) 易失性或部分易失性高 64 位 非易失性

入口 thunk 执行以下操作:

参数个数 堆栈使用
0-4 将 ARM64EC v6v7 存储到调用方分配的主空间中

由于被调用方是 ARM64EC,它没有主空间的概念,因此存储的值不会被破坏。

在堆栈上分配额外的 128 字节并存储 ARM64EC v8v15
5-8 x4 = 堆栈中的第 5 个参数
x5 = 堆栈中的第 6 个参数
x6 = 堆栈中的第 7 个参数
x7 = 堆栈中的第 8 个参数

如果参数是 SIMD,则改用 v4-v7 寄存器
+9 在堆栈上分配 AlignUp(NumParams - 8 , 2) * 8 个字节。 *

将第 9 个和剩余的参数复制到此区域

* 将值与偶数对齐,可以保证堆栈保持与 16 个字节对齐

如果函数接受 32 位的整数参数,则允许 thunk 仅推送 32 位而不是父寄存器的全部 64 位。

接下来,thunk 使用 ARM64 bl 指令调用 ARM64EC 函数。 函数返回后,thunk 执行以下操作:

  1. 撤消任何堆栈分配
  2. 调用 __os_arm64x_dispatch_ret 仿真器帮助程序以弹出 x64 返回地址并恢复 x64 仿真。

出口 thunk:ARM64EC 到 x64 函数调用

对于 ARM64EC C/C++ 函数对潜在的 x64 代码进行的每个调用,MSVC 工具链都会生成一个出口 thunk。 thunk 的内容取决于 x64 被调用方的参数以及被调用方是使用标准调用约定还是 __vectorcall。 编译器从被调用方的函数声明中获取此信息。

首先,thunk 推送 ARM64EC lr 寄存器中的返回地址和虚拟 8 字节值,以确保堆栈与 16 字节对齐。 其次,thunk 处理参数:

参数个数 堆栈使用
0-4 在堆栈上分配 32 个字节的主空间
5-8 在堆栈的更高位置再分配 AlignUp(NumParams - 4, 2) * 8 个字节。 *

将第 5 个和任何后续参数从 ARM64EC 的 x4-x7 复制到此额外的空间
+9 将第 9 个和剩余的参数复制到额外的空间

* 将值与偶数对齐,可以保证堆栈保持与 16 个字节对齐。

再次,thunk 调用 __os_arm64x_dispatch_call_no_redirect 仿真器帮助程序来调用 x64 仿真器以运行 x64 函数。 调用必须是 blr x16 指令(通常,x16 是易失性寄存器)。 需要 blr x16 指令,因为 x64 仿真器会将此指令解析为提示。

x64 函数通常尝试使用 x64 ret 指令返回到仿真器帮助程序。 此时,x64 仿真器检测到它在 ARM64EC 代码中。 然后,它读取前面的 4 字节提示,这恰好是 ARM64 blr x16 指令。 由于此提示指示返回地址位于此帮助程序中,因此仿真器会直接跳转到此地址。

x64 函数被允许使用任何分支指令返回到仿真器帮助程序,其中包括 x64 jmpcall。 另外,仿真器还处理这些情况。

然后,当帮助程序返回到 thunk 时,thunk 执行以下操作:

  1. 撤消任何堆栈分配
  2. 弹出 ARM64EC lr 寄存器
  3. 执行 ARM64 ret lr 指令。

ARM64EC 函数名称修饰

ARM64EC 函数名称在任何特定于语言的修饰之后采用了一个辅助修饰。 对于具有 C 链接的函数(无论是编译为 C 还是使用 extern "C" 编译),名称前会附加一个 #。 对于 C++ 修饰函数,名称中会插入一个 $$h 标记。

foo         => #foo
?foo@@YAHXZ => ?foo@@$$hYAHXZ

__vectorcall

ARM64EC 工具链目前不支持 __vectorcall。 编译器在检测到 ARM64EC 使用 __vectorcall 时,会发出错误。

另请参阅

了解 ARM64EC ABI 和汇编代码
Visual C++ ARM 迁移的常见问题
修饰名