ARM架构下函数调用与堆栈机制详解:从寄存器分配到内存布局

1次阅读
没有评论

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

image.webp

从两个崩溃案例说起

最近在调试 STM32H7 项目时,遇到两个典型堆栈问题:

ARM 架构下函数调用与堆栈机制详解:从寄存器分配到内存布局

  1. 递归爆栈:在图像处理算法中使用了深度优先搜索,递归层级超过默认 1KB 栈大小,导致 HardFault。通过 map 文件发现栈顶地址已侵入全局变量区

  2. 中断竞争:高优先级中断 ISR 修改了 R4-R11 寄存器,但未保存现场,返回后主程序出现寄存器值错乱。这是典型的 AAPCS 调用规范违反案例

ARM 与 x86 的关键差异

  1. 寄存器用途
  2. ARM:R0-R3 传参 /R12(IP)临时 /R13(SP)/R14(LR)/R15(PC)
  3. x86:EAX/ECX/EDX 通用,参数通过栈传递

  4. 参数传递

  5. ARM 前 4 个参数用寄存器(R0-R3),超出的压栈(右→左)
  6. x86 全部压栈(左→右)

  7. 栈增长方向

  8. ARM:满递减栈(FD, Full Descending)
  9. x86:通常也是向下增长

ARM 栈帧结构拆解

以 ARMv7- M 的栈帧为例:

High Address
+----------------+
|    参数 5~n     | <-- 调用者栈帧
+----------------+
|   Return Addr  |
+----------------+
|      LR        |
+----------------+
|      R7        | <- 帧指针(FP)
+----------------+
|  局部变量区    |
+----------------+
| 对齐填充(4 字节)|
+----------------+
|  保存的 R4-R6   |
+----------------+  
Low Address      | <-- 当前 SP

关键寄存器分工:
– SP:始终指向栈顶有效数据
– FP(R7):指向当前栈帧基址,方便调试器回溯调用链

启用栈保护编译:

arm-none-eabi-gcc -fstack-protector-strong -mcpu=cortex-m7

实战汇编代码分析

叶子函数示例(不调用其他函数):

add_two:          @ int add_two(int a, int b)
    ADD     R0, R0, R1  @ 直接使用 R0/R1 参数寄存器
    BX      LR          @ 无需操作栈

非叶子函数示例

factorial:        @ int factorial(int n)
    PUSH    {R4, LR}    @ 必须保存 LR 和用到的 R4
    MOV     R4, R0      @ 参数 n 保存到 R4
    CMP     R0, #1
    BLE     .Lbase_case
    SUB     R0, R0, #1
    BL      factorial   @ 递归调用
    MUL     R0, R4, R0
.Lbase_case:
    POP     {R4, PC}    @ 直接恢复 PC 相当于返回

获取当前栈指针技巧:

uint32_t get_sp() {register uint32_t sp asm("sp");
    asm volatile("":"=r"(sp));
    return sp;
}

性能优化实践

  1. 栈大小计算
  2. 静态分析:通过 arm-none-eabi-size 查看 .stack
  3. 动态检测:填充魔术字(0xDEADBEEF),运行时检查是否被改写

  4. 结构体传参优化

    // 避免这样传参(会拷贝到栈上)void draw_big_struct(struct widget w);
    
    // 改为指针传递
    void draw_big_struct(const struct widget *w);

安全防护方案

  1. 多任务栈隔离
  2. RTOS 中每个任务独立栈空间
  3. MPU 设置栈区域的读写权限

  4. 防栈溢出攻击

    # 禁止栈执行
    arm-linux-gnueabihf-gcc -z noexecstack
    
    # 启用栈金丝雀
    -fstack-protector-all

思考与进阶

  1. 中断优先级切换时,如何设计栈共享方案避免内存浪费?
  2. Cortex- M 的自动压栈机制与 A 系列的手动保存有何本质区别?
  3. 如何用 MPU 在 RTOS 中实现精确到 128 字节的栈边界保护?

通过 GDB 观察栈变化是最直观的学习方式,尝试单步执行以下命令:

maint print stack
frame
info registers sp lr pc

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