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 /SS 1、2 |
RFLAGS 子集:SF /ZF /CF /OF /TF |
volatile | volatile | volatile |
不可用 | RFLAGS 子集:PF /AF |
空值 | 空值 | volatile |
不可用 | RFLAGS 子集:DF |
空值 | 空值 | 非易失性 |
1 避免直接读取、写入或计算 PSTATE
和 RFLAGS
之间的映射。 这些位可能在将来使用,并且可能会发生更改。
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) |
FPCR 2 |
MXCSR[15:6] |
非易失性 | 非易失性 | 非易失性 |
FPSR 2 |
MXCSR[5:0] |
volatile | volatile | volatile |
1 这些 ARM64 寄存器的特殊之处在于低 64 位是非易失性的,而高 64 位是易失性的。 从 x64 调用方的角度来看,它们实际上是易失性的,因为被调用方会回收数据。
2 避免直接读取、写入或计算 FPCR
和 FPSR
的映射。 这些位可能在将来使用,并且可能会发生更改。
结构打包
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 -x8 、x15 (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 -x8 、x15 (chkstk ) 和 q0 -q7 |
__os_arm64x_check_icall_cfg |
与 __os_arm64x_check_icall 相同,但还检查指定的地址是否为有效的控制流图间接调用目标 |
参数:x10 :出口 thunk 的地址x11 :目标函数的地址Out: x9 :如果目标为 x64,则为函数的地址。 其他情况下则不定义x10 :出口 thunk 的地址x11 :如果目标为 x64,则它包含出口 thunk 的地址。 否则,为函数的地址保留的寄存器: x0 -x8 、x15 (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 v6 和 v7 存储到调用方分配的主空间中由于被调用方是 ARM64EC,它没有主空间的概念,因此存储的值不会被破坏。 在堆栈上分配额外的 128 字节并存储 ARM64EC v8 到 v15 。 |
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 执行以下操作:
- 撤消任何堆栈分配
- 调用
__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 jmp
和 call
。 另外,仿真器还处理这些情况。
然后,当帮助程序返回到 thunk 时,thunk 执行以下操作:
- 撤消任何堆栈分配
- 弹出 ARM64EC
lr
寄存器 - 执行 ARM64
ret lr
指令。
ARM64EC 函数名称修饰
ARM64EC 函数名称在任何特定于语言的修饰之后采用了一个辅助修饰。 对于具有 C 链接的函数(无论是编译为 C 还是使用 extern "C"
编译),名称前会附加一个 #
。 对于 C++ 修饰函数,名称中会插入一个 $$h
标记。
foo => #foo
?foo@@YAHXZ => ?foo@@$$hYAHXZ
__vectorcall
ARM64EC 工具链目前不支持 __vectorcall
。 编译器在检测到 ARM64EC 使用 __vectorcall
时,会发出错误。