ARM函数调用规范详解:从寄存器分配到异常处理

1次阅读
没有评论

共计 1633 个字符,预计需要花费 5 分钟才能阅读完成。

image.webp

ARM 函数调用规范详解:从寄存器分配到异常处理

在嵌入式开发中,尤其是 RTOS 场景下,理解 ARM 架构的过程调用标准(AAPCS)至关重要。它确保了中断嵌套时的寄存器安全、跨语言调用的兼容性,以及栈空间的正确管理。今天我们就来深入这个平时容易被忽视,却直接影响系统稳定性的基础规范。

ARM 函数调用规范详解:从寄存器分配到异常处理

为什么需要关注 AAPCS?

当你的代码中同时存在 C 和汇编,或者需要处理中断嵌套时,如果不知道 R4-R11 是 callee-saved 寄存器,就可能出现随机性的寄存器值被覆盖。更隐蔽的问题是栈帧不对齐导致的性能下降——ARMv7 要求 8 字节栈对齐,违规则可能触发硬件异常。

与 x86 的显著差异

x86 架构主要依靠栈传递参数,而 ARM 则有明确的寄存器分级:

  • 前 4 个整型参数通过 R0-R3 传递(x86 用栈)
  • 浮点参数使用 S0-S15(x87 栈在 x86 上已被淘汰)
  • 返回地址存于 LR(x86 压入栈)

这种设计使 ARM 在叶子函数调用时能减少内存访问,但对寄存器分配提出了更高要求。

核心规则图解

寄存器使用分类

寄存器 角色 保存责任方
R0-R3 参数 / 返回值 Caller
R4-R8 局部变量 Callee
R9 平台特定(如 TLS) 特殊
R10-R11 局部变量 Callee
R12 临时(IP) Caller
R13 栈指针(SP) 必须保持
R14 链接寄存器(LR) Caller
R15 程序计数器(PC)

典型栈帧布局

高地址
+----------------+
| 入参 5~N        | ← Caller 的栈空间
+----------------+
| 返回地址       | ← LR 保存位置
+----------------+
| 旧的 FP         | ← R7/R11 指向这里
+----------------+
| R4-R8 局部变量  | ← Callee 的栈空间
+----------------+
低地址           | ← SP 当前指向

代码实例解析

非叶子函数标准序言

my_func:
    PUSH    {r4-r7, lr}      @ 保存必须保留的寄存器
    ADD     r7, sp, #12      @ 设置帧指针(R7 指向 LR 存储位置)
    SUB     sp, sp, #16      @ 分配局部变量空间(保持 8 字节对齐!)
    ... 函数体 ...
    MOV     r0, #42          @ 返回值置入 R0
    ADD     sp, sp, #16      @ 回收栈空间
    POP     {r4-r7, pc}      @ 恢复寄存器并返回(通过弹出 PC)

关键点说明:
– 帧指针(R7/R11)用于调试器回溯调用栈
– POP 到 PC 等效于BX LR,但能同时恢复寄存器

中断服务例程特例

ISR_Handler:
    PUSH    {r0-r3, r12, lr} @ 必须保存 caller-saved 寄存器
    MRS     r0, SPSR         @ 保存状态寄存器
    PUSH    {r0}            
    ... 中断处理 ...
    POP     {r0}
    MSR     SPSR_cxsf, r0    @ 恢复状态
    POP     {r0-r3, r12, lr}
    SUBS    pc, lr, #4       @ 异常返回(注意 LR 修正)

浮点处理的坑

当使用软浮点(softfp)时:
– 双精度参数占用两个连续寄存器(如 R0+R1)
– Q0-Q7 需要 128 位对齐,在函数调用间可能被破坏
– 推荐编译选项:-mfloat-abi=softfp -mfpu=vfpv3

实战调试技巧

  1. 用 objdump 验证调用约定:

    arm-none-eabi-objdump -d your.elf | grep -A 20 "<func>:"

    检查 PUSH/POP 序列是否匹配规范

  2. GDB 动态观察栈指针:

    (gdb) break *0x08000123
    (gdb) commands
     > print/x $sp
     > print/x $lr
     > continue
     > end

三个进阶思考

  1. 可变参数函数实现:靠 R0-R3 传递固定参数,剩余参数通过栈访问,需用 __builtin_va_list 系列宏
  2. TrustZone 变化:安全世界和非安全世界有独立的 SP 和 LR,切换时需要显式保存状态
  3. RISC- V 对比:更简单的规范(只有 a0-a7 参数寄存器),但缺乏 ARM 的严格分层设计

理解这些规范的本质,是为了在遵守规则的同时,知道何时可以谨慎地突破——比如在性能关键的叶子函数中手工优化寄存器使用。这才是嵌入式开发的精髓所在。

正文完
 0
评论(没有评论)