ARM 函数调用时参数和返回值到底放在哪里?AAPCS 协议详解与实验平台实战
时间:2026-11-06 来源:华清远见
在嵌入式 C 语言开发中,我们经常编写函数并传递参数。但在底层的 ARM 汇编层面,这些参数是如何从调用者传递给被调用者的?返回值又是如何带回的?这一切都遵循着一套严格的“交通规则”——AAPCS。本文将结合华清远见元宇宙实验平台的 Cortex-M4 内核寄存器视图,带你通过可视化的方式彻底搞懂 ARM 函数调用的底层秘密。

一、什么是 AAPCS?
AAPCS,全称是 ARM Architecture Procedure Call Standard(ARM 架构过程调用标准)。如果把 CPU 的寄存器比作高速公路上的车道,那么 AAPCS 就是交通法规。它规定了在 ARM 处理器上,当一段代码(调用者)去呼叫另一段代码(被调用者)时,双方必须遵守的底层协议。如果没有这套标准,不同的编译器(如GCC和Keil)生成的代码将无法互相配合,C语言也无法顺利调用汇编代码。
核心作用:
统一参数传递规则:规定前几个参数放寄存器,剩下的放栈。
统一返回值规则:规定结果放在哪个寄存器带回来。
寄存器保护责任:明确哪些寄存器由调用者保存,哪些由被调用者保存(防止数据被意外覆盖)。
二、ARM 函数调用时,参数和返回值到底放在哪里?
在经典的32位ARM (ARMv7-M,如 Cortex-M3/M4) 架构中,AAPCS对16个通用寄存器(R0-R15)的使用进行了严格分工。我们可以结合华清远见元宇宙实验平台左侧的“Cortex-M4 处理器内核寄存器”面板来直观理解。

1. 参数的传递位置
前 4 个参数:优先通过通用寄存器 R0, R1, R2, R3 传递。
第 1 个参数 → R0
第 2 个参数 → R1
第 3 个参数 → R2
第 4 个参数 → R3
超过 4 个的参数:第 5 个及之后的参数,将通过栈(Stack) 进行传递(通常遵循从右向左的顺序压栈)。
在实验平台的寄存器面板中,你可以清晰地看到 R0 - R3 位于“通用寄存器”区域的最上方。这四个寄存器是函数调用的“VIP 通道”,存取速度最快。
2. 返回值的位置
位及以内的整型/指针数据:通常通过寄存器 R0 返回。
位数据(如 long long、double):通过寄存器 R0 和 R1 联合返回。
大型结构体:通常由调用者在栈上预留空间,并将该空间的地址作为隐藏的第一个参数(放入R0)传递给被调用函数。
3. 特殊寄存器的角色
除了通用寄存器,还有两个关键寄存器参与函数调用:
SP (R13):栈指针。用于存放超过4个的参数以及局部变量。
LR (R14):链接寄存器。非常重要!它存放了函数执行完毕后的返回地址。当函数执行完 BX LR 指令时,CPU就会跳回这里继续执行。
三、实战演练:在元宇宙实验平台验证 AAPCS
为了验证上述理论,我们编写一段简单的C语言测试代码,并在华清远见元宇宙实验平台中进行单步调试观察。
测试代码
#include <stdio.h>
// 定义一个有5个参数的函数,测试寄存器传参和栈传参的边界
int add_numbers(int a, int b, int c, int d, int e)
{
int sum = a + b + c + d + e;
return sum;
}
int main( )
{
// 传入 1, 2, 3, 4, 5
int result = add_numbers(1, 2, 3, 4, 5);
printf("Result: %d\n", result);
return 0;
}
调试观察步骤
请在实验平台中加载上述代码,并在 add_numbers 函数入口处设置断点,观察以下现象:
1. 观察 R0 - R3(前四个参数)
当程序停在 add_numbers 函数内部时,查看左侧寄存器面板:
R0 的值应该是 0x00000001 (对应参数 a)
R1 的值应该是 0x00000002 (对应参数 b)
R2 的值应该是 0x00000003 (对应参数 c)
R3 的值应该是 0x00000004 (对应参数 d)
2. 观察栈内存(第五个参数)
由于只有 4 个参数寄存器,第 5 个参数 e (值为 5) 会被压入栈中。
查看 SP (R13) 寄存器的值,这是当前栈顶地址。
在平台的内存查看窗口中,找到 SP 指向的地址(通常是 SP + 某个偏移量,取决于编译器对齐),你会发现数值 5 存储在那里。
3. 观察 LR (R14)(返回地址)
查看 LR (R14) 寄存器。它的值是一个内存地址(例如 0x0800xxxx)。
这个地址指向的是 main 函数中调用 add_numbers 之后的那条指令(即 printf 那一行)。这就是函数执行完后“回家”的路。
4. 观察返回值
单步执行直到函数返回。
你会发现在函数返回后,R0 的值变成了 0x0000000F (即十进制的 15),这就是计算结果 1+2+3+4+5 的返回值。
函数调用全过程图解:
以 int result = add(1, 2, 3, 4, 5, 6); 为例,展示 AAPCS 下的完整调用流程:
┌─────────────────────────────────────────────────────
│ 调用者(Caller) │
│ MOV R0, #1 ← 第1个参数 │
│ MOV R1, #2 ← 第2个参数 │
│ MOV R2, #3 ← 第3个参数 │
│ MOV R3, #4 ← 第4个参数 │
│ PUSH {R5, R6} ← 第5、6个参数压栈 │
│ BL add ← 跳转,LR = 返回地址 │
│ ADD R5, R0, #0 ← 从 R0 取返回值 │
│ POP {R5, R6} ← 恢复栈 │
└─────────────────────────────────────────────────────┘
↓ 调用 ↓
┌──────────────────────────────────────────────────────
│ 被调用者(Callee) │
│ PUSH {R4-R7, LR} ← 序言:保存 callee-saved 寄存器 + LR │
│ ADD R0, R0, R1 ← 从 R0, R1 取前两个参数 │
│ ADD R0, R0, R2 ← 从 R2 取第3个参数 │
│ ADD R0, R0, R3 ← 从 R3 取第4个参数 │
│ LDR R4, [SP, #xx] ← 从栈中取第5、6个参数 │
│ ADD R0, R0, R4 ← 累加 │
│ POP {R4-R7, PC} ← 尾声:恢复寄存器,弹出 LR→PC 返回 │
└────────────────────────────────────────────────────┘
四、实战建议及总结
实战建议
参数尽量不超过4个:超过4个会触发栈操作,增加指令数和执行时间。
位参数对齐:long long 类型必须放在偶数寄存器对(R0:R1 或 R2:R3),不能放在 R1:R2。
调试 HardFault 时查栈帧:当程序崩溃时,通过查看 SP 指向的栈帧内容,可以还原调用链和参数值。
中断服务函数特殊处理:中断发生时,硬件自动将 R0–R3、R12、LR、PC、xPSR 压栈(共8个字),ISR 返回时自动出栈。
总结
通过华清远见元宇宙实验平台的可视化调试,我们将抽象的 AAPCS 协议变成了看得见的数据流动:

掌握 AAPCS 不仅有助于理解 C 语言底层机制,更是进行嵌入式汇编混合编程、RTOS 任务切换以及系统级 Debug 的基石。

