共计 1868 个字符,预计需要花费 5 分钟才能阅读完成。
背景痛点
在 ARM 嵌入式开发中,函数调用规约(Calling Convention)是确保程序正确运行的基础。然而,许多新手开发者往往会忽略这一点,导致一些难以调试的问题。比如:

- 寄存器污染:函数调用后,某些寄存器的值被意外修改,导致调用方的逻辑出错。
- 栈不对齐:ARM 架构要求栈指针(SP)在某些情况下必须 8 字节对齐,否则可能触发 HardFault 异常。
- 参数传递混乱:在混合调用 C 和汇编时,如果参数传递规则不明确,会导致数据读取错误。
这些问题通常表现为程序崩溃或数据错误,且难以通过常规调试手段发现。因此,理解 ARM 的函数调用规约(AAPCS)是嵌入式开发的必备技能。
标准解析:AAPCS 与 ATPCS
ARM 架构的函数调用规约主要有两种:AAPCS(ARM Architecture Procedure Call Standard)和 ATPCS(ARM-Thumb Procedure Call Standard)。AAPCS 是当前的主流标准,而 ATPCS 主要用于早期的 Thumb 模式。以下是 AAPCS 的核心规则:
- 寄存器分配:
- 参数传递 :前 4 个整型参数通过
r0-r3传递,多余的参数通过栈传递。 - 返回值 :整型返回值通过
r0返回,64 位值通过r0和r1返回。 - 调用者保存寄存器 :
r0-r3和r12是调用者保存(Caller-saved),函数可以自由使用这些寄存器,但调用方需负责保存它们。 -
被调用者保存寄存器 :
r4-r11是被调用者保存(Callee-saved),函数如果使用这些寄存器,必须在返回前恢复它们的值。 -
栈对齐:
- AAPCS 要求栈指针(SP)在函数入口和出口时必须 8 字节对齐(ARMv7- M 架构)。
-
中断服务程序(ISR)中必须强制 8 字节对齐,否则可能触发 HardFault。
-
浮点寄存器(VFP):
- 如果使用硬件浮点,浮点参数通过
s0-s15传递,返回值通过s0返回。 - 浮点寄存器
s16-s31是被调用者保存的。
代码实战:混合调用 C 与汇编
以下是一个 ARMv7- M 架构下的汇编与 C 混合编程示例,展示如何正确保存和恢复寄存器,并遵守栈对齐规则:
; 汇编函数示例:调用 C 函数并保存寄存器
.global asm_function
asm_function:
PUSH {r4-r6, lr} ; 保存被调用者保存寄存器和 LR
SUB sp, sp, #8 ; 为局部变量预留栈空间(保持 8 字节对齐); 调用 C 函数
MOV r0, #42 ; 第一个参数通过 r0 传递
MOV r1, #100 ; 第二个参数通过 r1 传递
BL c_function ; 调用 C 函数
ADD sp, sp, #8 ; 恢复栈指针
POP {r4-r6, pc} ; 恢复寄存器并返回
; C 函数示例
int c_function(int a, int b) {return a + b;}
栈帧示意图
以下是函数调用时的栈帧布局:
高地址
|----------------|
| LR | <- 调用方的返回地址
|----------------|
| r6 | <- 被调用者保存寄存器
|----------------|
| r5 |
|----------------|
| r4 |
|----------------|
| 局部变量区 | <- 8 字节对齐
|----------------|
低地址
避坑指南
- 中断服务程序(ISR)中的栈对齐:
-
在 ISR 中,必须手动确保栈对齐。例如,在 Keil 中可以使用
__attribute__((aligned(8)))修饰 ISR 函数。 -
编译选项校验:
- 在 Keil 中,启用
--strict选项可以检查 AAPCS 合规性。 -
在 IAR 中,使用
--aeabi选项确保符合 AAPCS 标准。 -
常见错误:
- 忘记保存 LR 寄存器:在非叶子函数中,LR 寄存器必须保存到栈中,否则返回地址会丢失。
- 栈指针未对齐:在函数入口和出口时,SP 必须 8 字节对齐。
- 浮点寄存器未保存:如果使用 VFP,必须正确保存
s16-s31。
性能考量
- 叶子函数优化:
- 叶子函数(不调用其他函数的函数)可以不保存 LR 寄存器,减少栈开销。
-
尽量使用
r0-r3传递参数,避免不必要的栈操作。 -
减少栈使用:
- 优先使用寄存器而非栈存储局部变量。
- 将频繁调用的函数设计为叶子函数。
思考题
- 如何设计可变参数函数(如
printf)的调用约定? - 在中断嵌套场景下,如何确保栈对齐和寄存器保存的正确性?
- 如果函数需要返回一个结构体,AAPCS 规定如何传递返回值?
总结
ARM 函数调用规约是嵌入式开发的基础知识,正确理解并应用 AAPCS 可以避免许多隐蔽的错误。本文从寄存器分配、栈帧管理到混合编程示例,逐步解析了 ARM 函数调用的核心规则。希望读者能在实际开发中灵活运用这些技巧,写出高效、稳定的嵌入式代码。
