ARM64(AArch64)的函数调用规则主要遵循 AAPCS64 (Procedure Call Standard for the ARM 64-bit Architecture)。了解这些规则对于编写汇编代码、调试程序或进行性能优化至关重要。
以下是 ARM64 函数调用中寄存器的存储与使用规则详解:
1. 通用寄存器 (X0 - X30)
ARM64 有 31 个 64 位通用寄存器,其用途分为:调用者保存 (Caller-saved) 和 **被调用者保存 (Callee-saved)**。
| 寄存器 | 别名 | 描述与规则 | 保存者 |
|---|---|---|---|
| X0 - X7 | 参数寄存器 | 用于传递前 8 个整数或指针参数。X0 也用于存放返回值。 | Caller |
| X8 | XR (Indirect result) | 间接结果位置寄存器。用于返回大型结构体(由调用者分配空间)。 | Caller |
| X9 - X15 | 临时寄存器 | 任意使用的临时变量。 | Caller |
| X16 - X17 | IP0, IP1 | 过程内调用寄存器。通常由链接器(Linker)在插入桩代码(Veneers)时使用。 | Caller |
| X18 | PR (Platform) | 平台寄存器。保留给操作系统或特定平台使用(如在 iOS 中被禁用或保留)。 | - |
| X19 - X28 | 临时寄存器 | 必须在函数返回前恢复原样。 | Callee |
| X29 | FP (Frame Pointer) | 帧指针。指向当前栈帧的底部,用于回溯堆栈。 | Callee |
| X30 | LR (Link Register) | 链接寄存器。存储函数返回时的跳转地址(即 ret 指令跳转的位置)。 |
Callee |
| SP | Stack Pointer | 栈指针。始终保持 16 字节对齐。 | - |
| XZR | Zero Register | 常数 0。只读,写入无效。 | - |
2. 浮点/向量寄存器 (V0 - V31)
ARM64 提供 32 个 128 位寄存器,用于浮点数(Float/Double)和 SIMD(NEON)指令。
- V0 - V7: 用于传递浮点参数和返回浮点值(Caller-saved)。
- V8 - V15: **被调用者保存 (Callee-saved)。但注意:仅需保存低 64 位 (D8-D15)**。如果函数使用了这些寄存器的 128 位,则高 64 位不需要被恢复。
- V16 - V31: 临时寄存器(Caller-saved)。
3. 参数传递规则
- 整数/指针参数:
- 前 8 个参数通过 X0 - X7 传递。
- 如果参数超过 8 个,剩余参数通过 栈 (Stack) 传递。
- 浮点参数:
- 前 8 个浮点/向量参数通过 V0 - V7 传递。
- 多余的通过栈传递。
- 结构体传递:
- 小型结构体(通常 <= 16字节)可以直接放在 X0-X1 中。
- 大型结构体由调用者在内存中准备好,将地址通过 X8 传入函数。
4. 返回值规则
- 整数/指针:存放在 X0 中。如果是 128 位的结果,则使用 X0 和 X1。
- 浮点数:存放在 V0 中。
- 大型结构体:函数将结果写入 X8 指向的内存区域。
5. 栈 (Stack) 的约束
- 16 字节对齐:在任何对外调用(函数调用)发生时,SP (Stack Pointer) 必须是 16 字节对齐的。否则会导致硬件异常(Alignment Fault)。
- 向下增长:栈向低地址方向增长。
- Red Zone:与 x86-64 不同,ARM64 标准 没有 Red Zone(即 SP 指向的地址以下是不安全的,随时可能被信号处理程序覆盖)。
6. 函数调用时的标准动作(汇编视角)
函数开始(Prologue):
1 | stp x29, x30, [sp, #-32]! // 将 FP 和 LR 压入栈,并向下移动 SP 32 字节 |
函数返回(Epilogue):
1 | ldp x19, x20, [sp, #16] // 恢复被保存的寄存器 |
总结核心记忆点
- X0-X7:传参和返回值。
- X19-X28:如果你用了,你就得负责存和取(Callee-saved)。
- **X30 (LR)**:存的是回家的路。
- SP:时刻保持 16 字节对齐。
- V8-V15:浮点数的“非易失”区,但只保一半(64bit)。