共计 1311 个字符,预计需要花费 4 分钟才能阅读完成。
背景痛点:栈操作引发的血案
刚接触 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} 时:
- SP 先向下移动 20 字节(5 个寄存器×4 字节)
- 依次存储 R0 到 R3
- 最后存储 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
思考进阶
- 中断嵌套时如何保证栈不溢出?
- 为什么 AAPCS 要求 8 字节栈对齐?
希望这篇笔记能帮你避开那些年我踩过的栈坑!下次遇到 HardFault 时,不妨先用 info registers 看看 SP 和 LR 的值。
正文完
