共计 1324 个字符,预计需要花费 4 分钟才能阅读完成。
在嵌入式开发和系统编程中,理解 ARM 函数调用规约(Calling Convention)是确保代码正确交互的基石。无论是 JNI/FFI 跨语言调用,还是内核开发中的汇编优化,掌握 AAPCS 标准都能避免参数传递混乱、寄存器误用等隐蔽问题。本文将从实际应用出发,带你深入理解 ARM 架构下的调用规则。

ARM 与 x86 调用约定的核心差异
参数传递方式 是两大架构最显著的差异点:
- ARM(AAPCS)使用 R0-R3 寄存器传递前 4 个整型参数,剩余参数通过栈传递
- x86 则主要依赖栈传递参数(fastcall 除外),仅使用 EAX/EDX 传递少量参数
返回值存放也有明显区别:
| 架构 | 整型返回值存放 | 浮点返回值存放 | 栈对齐要求 |
|---|---|---|---|
| ARM | R0 | S0/D0 | 8 字节 |
| x86 | EAX | ST(0)/XMM0 | 4 字节 |
栈对齐要求 的差异常导致跨平台问题:
- ARMv7 要求 SP 在函数入口处保持 8 字节对齐
- ARMv8 将这个要求扩展到 16 字节对齐
- x86 通常只需 4 字节对齐
典型调用场景汇编示例
整数参数传递示例(ARMv7)
/* 函数调用示例 */
main:
mov r0, #1 @ 第一个参数
mov r1, #2 @ 第二个参数
mov r2, #3 @ 第三个参数
mov r3, #4 @ 第四个参数
push {r4, lr} @ 保存调用者寄存器
bl my_func @ 调用函数
pop {r4, pc} @ 恢复寄存器并返回
/* 被调用函数 */
my_func:
add r0, r0, r1 @ 使用 R0-R3
add r0, r0, r2
add r0, r0, r3
bx lr @ 返回
VFP/SIMD 寄存器使用规则
当使用硬件浮点时:
- 单精度浮点使用 S0-S15 传递
- 双精度浮点使用 D0-D7 传递
- 超过 16 个单精度 / 8 个双精度参数时转用栈传递
混合编程常见错误
错误案例 1:LR 寄存器未保存
// C 代码声明
void asm_func(void);
// 错误汇编实现
.global asm_func
asm_func:
bx lr // 错误!未保存原始 LR
正确做法:
.global asm_func
asm_func:
push {lr} // 必须保存返回地址
...
pop {pc} // 通过 PC 实现返回
错误案例 2:SP 未对齐导致 NEON 崩溃
// 调用 NEON 指令前未对齐栈
void neon_op(float *data) {__asm__("vld1.32 {d0}, [%0]" : : "r"(data));
}
解决方案:
- 使用 GCC 的
__attribute__((aligned(8)))保证数据对齐 - 在汇编代码中手动调整 SP:
bic sp, sp, #7
编译与验证建议
推荐 GCC 编译选项:
-mabi=aapcs:强制使用 AAPCS 标准-mapcs-frame:生成符合规范的栈帧-mfloat-abi=hard:启用硬件浮点调用
验证方法:
arm-linux-gnueabihf-gcc -S -o test.s test.c # 生成汇编
objdump -d a.out # 反汇编验证
进阶思考
设计可变参数函数的 ABI 方案时需要考虑:
- 如何保证参数列表的可扩展性
- 处理浮点参数的寄存器 / 栈混合传递
- 实现 va_start/va_arg 时的栈帧遍历规则
理解这些底层机制,能帮助开发者编写出更健壮的跨平台代码,特别是在嵌入式系统开发和性能优化场景中。通过实际反汇编验证理论认知,是掌握调用规约最有效的方式。
正文完
