ARM函数调用的压栈过程详解:从寄存器分配到栈帧布局

1次阅读
没有评论

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

image.webp

背景痛点:栈操作引发的血案

刚接触 ARM 开发的同学们肯定遇到过这些诡异场景:

ARM 函数调用的压栈过程详解:从寄存器分配到栈帧布局

  • 程序跑着跑着突然 HardFault,调试发现 SP 指针指向了非法地址
  • 中断服务程序里调用函数后,主程序寄存器值被莫名修改
  • 递归函数超过 5 层就死机,改成循环却正常

这些问题 80% 都与栈使用不当有关。比如在 STM32 裸机开发中:

void ISR_Handler() {char buffer[256]; // 中断栈爆了!parse_data(buffer); 
}

ARM 调用规范必修课

ARM 架构有两种执行状态,压栈方式也不同:

  • ARM 模式 :每次压栈 4 字节对齐,常用STMFD 指令(Store Multiple Full Descending)
  • Thumb 模式 :支持 2 字节压栈,用PUSH 指令更节省空间

根据 AAPCS 规范,函数调用时需要保护这些寄存器:

High Address
    ┌─────────┐
    │   LR    │ ← 必须保存
    ├─────────┤
    │   R11   │
    ├─────────┤
    │   ...   │ ← R4-R11 根据需要使用
    ├─────────┤
    │   R4    │
    └─────────┘
Low Address

栈帧构建全流程

当执行 push {r0-r3, lr} 时:

  1. SP 先向下移动 20 字节(5 个寄存器×4 字节)
  2. 依次存储 R0 到 R3
  3. 最后存储 LR 寄存器

用 ASCII 图表示内存变化:

执行前:        执行后:
[栈顶]         [栈顶]
| ... |        | R0  |
|     |        | R1  |
|     |        | R2  |
|     |        | R3  |
|     |        | LR  |

FP 寄存器通常指向当前栈帧底部,方便局部变量访问:

main:
    push {fp, lr}   @ 4 周期
    add fp, sp, #4  @ 1 周期
    sub sp, #16     @ 为局部变量腾空间

动手验证环节

用 GCC 内联汇编观察实际效果:

__attribute__((optimize("-O1"))) 
void test_func(int a) {
    asm volatile("push {r4, r5}\\n"
        "mov r4, #1\\n"
        "mov r5, #2\\n"
        "pop {r4, r5}"
    );
}

GDB 调试命令:

(gdb) disassemble test_func
(gdb) x/8x $sp  @ 查看栈内容

避坑实战指南

案例 1:递归深度爆炸

  • 问题:递归计算斐波那契导致栈溢出
  • 解决:改用迭代实现,或手动增大栈空间
#define STACK_SIZE 1024 * 8  @ 8KB 栈
StackType_t taskStack[STACK_SIZE];

案例 2:中断服务程序

  • 问题:ISR 调用库函数破坏寄存器
  • 解决:添加 __attribute__((naked)) 并手动保存寄存器
__attribute__((naked)) void TIM_IRQHandler() {asm("push {r4-r7, lr}");
    // 中断处理代码
    asm("pop {r4-r7, pc}");
}

案例 3:多任务栈分配

  • 问题:RTOS 任务栈不足
  • 解决:通过反汇编计算最大栈深度
arm-none-eabi-objdump -d app.elf | \
    grep 'sub.*sp' | sort -n

思考进阶

  1. 中断嵌套时如何保证栈不溢出?
  2. 为什么 AAPCS 要求 8 字节栈对齐?

希望这篇笔记能帮你避开那些年我踩过的栈坑!下次遇到 HardFault 时,不妨先用 info registers 看看 SP 和 LR 的值。

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