Hotdry.

Article

跨平台 ARM64 汇编的调用约定抽象

剖析 Linux、macOS、Windows 三平台 ARM64 调用约定的关键差异,提供可落地的条件编译策略与寄存器使用规范。

2026-06-03systems

在 ARM64 架构逐渐统一服务器与桌面市场的背景下,编写能够在 Linux、macOS 和 Windows 三平台无缝运行的汇编代码成为系统级开发者面临的现实挑战。尽管 AArch64 架构遵循统一的 AAPCS64(ARM Architecture Procedure Call Standard)规范,但各操作系统在参数对齐、栈帧布局和系统调用接口上的实现差异,足以让一份看似标准的汇编代码在不同平台产生难以调试的运行时错误。

调用约定的隐性分歧

AAPCS64 规范定义了寄存器使用的基本框架:X0-X7 用于传递整数和指针参数,V0-V7 用于浮点参数,X0 存放返回值。然而,当参数需要溢出到栈上时,平台间的差异开始显现。

Linux 遵循严格的 8 字节对齐策略。无论参数实际大小如何,每个栈槽占用 8 字节,int(4 字节)和 short(2 字节)类型的参数在寄存器中时,高位比特处于未定义状态。这种设计简化了栈指针计算,但意味着调用者不能假设小整型参数的高位被清零或符号扩展。

macOS 采取了不同的对齐策略。参数按其声明大小对齐:int 类型 4 字节对齐,short 类型 2 字节对齐。这种差异在 JNI 等跨语言调用场景中尤为致命 —— 当 Java 层传递的 classData 指针在 macOS 上被错误解析为 0xa 而非有效地址时,根源往往在于栈偏移量的计算错位。在 OpenJDK 的 macOS-AArch64 移植过程中,修复此类调用约定不匹配需要将栈偏移增量从固定的 wordSize 改为条件编译的 4 字节。

Windows 的经典 ARM64 调用约定与 Linux 基本一致,但引入了 ARM64EC(Emulation Compatible)变体用于与 x64 代码互操作。EC 模式下的寄存器分配和栈帧布局需要额外考虑与模拟层的兼容性,这要求汇编代码在编译期识别目标子平台。

系统调用接口的分化

用户态程序与内核的交互方式在三平台上呈现显著差异。Linux 提供直接的 syscall 指令接口:系统调用号置于 X8,参数依次放入 X0-X5,执行 syscall 指令后返回值位于 X0。这种设计使得裸金属级别的系统调用包装器实现相对直接。

macOS 和 Windows 则不鼓励用户态代码直接发起系统调用。macOS 的底层 ABI 包含额外的状态转换要求,而 Windows 的系统调用通常通过 ntdll 或内核 API 层封装。对于需要直接操作内核接口的汇编代码,这意味着必须提供平台特定的抽象层,而非假设 syscall 指令的通用性。

工程化实践:条件编译与抽象层

处理平台差异的核心策略是在编译期通过预处理器宏隔离平台特定代码。以下模式在实践中被验证有效:

寄存器保存策略:AAPCS64 规定 X19-X28 为被调用者保存寄存器,但各平台对浮点寄存器的保存要求存在细微差别。在函数序言中显式保存 V8-V15 是确保跨平台安全的保守做法。

栈对齐检查:调用指令执行前栈指针必须 16 字节对齐。在调试构建中插入断言验证 sp & 0xF == 0 可以提前捕获对齐错误,避免在复杂调用链中定位问题的困难。

参数打包规则:对于包含混合类型(整数与浮点)的结构体参数,需要理解 homogeneous floating-point aggregate(HFA)规则。完全由 float 或 double 组成的结构体优先使用浮点寄存器传递,而非整数寄存器。这一规则在跨平台代码中容易引发寄存器分配错误。

变参函数处理:变参函数在三平台上遵循特殊规则 —— 浮点参数被当作整数处理,单精度浮点按 32 位整数传递,双精度按 64 位整数传递。实现 printf 风格的包装函数时必须考虑这一转换。

可落地的检查清单

编写可移植 ARM64 汇编时,建议按以下清单逐项验证:

  1. 参数对齐验证:对于 macOS 目标,确认小于 8 字节的栈参数使用其声明大小对齐;对于 Linux/Windows,确认使用 8 字节对齐。

  2. 寄存器使用审计:检查 X0-X7 和 V0-V7 的参数分配是否符合平台预期;确认 X19-X28 和 V8-V15 在函数返回前已恢复。

  3. 栈帧布局检查:函数序言中预留的栈空间需包含被调用者保存寄存器、局部变量和 outgoing 参数区域的总和,且最终大小向上对齐至 16 字节。

  4. 系统调用抽象:避免在通用代码中硬编码 syscall 指令;通过平台特定的宏或 C 包装器隔离系统调用路径。

  5. 变参处理审查:变参函数的浮点参数必须按整数规则处理,确保调用方和实现方使用一致的寄存器分配策略。

  6. 结构体传递测试:对于小于等于 16 字节的结构体,验证其按值传递时的寄存器打包行为;对于更大结构体,确认按指针传递的约定。

跨平台 ARM64 汇编的复杂性不在于指令集本身,而在于 ABI 层面的微妙差异。通过建立清晰的平台抽象层和严格的编译期检查,可以将这些差异隔离在可控范围内,实现真正的 "一次编写,多平台运行"。


参考来源

  • Ludovic Henry, "Differences in Calling Conventions", 2020
  • Raymond Chen, "The AArch64 processor (aka arm64), part 20: The classic calling convention", Microsoft Developer Blog, 2022
  • ARM Limited, "Procedure Call Standard for the ARM 64-bit Architecture (AArch64)"

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com