共计 1551 个字符,预计需要花费 4 分钟才能阅读完成。
从两个崩溃案例说起
最近在调试 STM32H7 项目时,遇到两个典型堆栈问题:

-
递归爆栈:在图像处理算法中使用了深度优先搜索,递归层级超过默认 1KB 栈大小,导致 HardFault。通过 map 文件发现栈顶地址已侵入全局变量区
-
中断竞争:高优先级中断 ISR 修改了 R4-R11 寄存器,但未保存现场,返回后主程序出现寄存器值错乱。这是典型的 AAPCS 调用规范违反案例
ARM 与 x86 的关键差异
- 寄存器用途:
- ARM:R0-R3 传参 /R12(IP)临时 /R13(SP)/R14(LR)/R15(PC)
-
x86:EAX/ECX/EDX 通用,参数通过栈传递
-
参数传递:
- ARM 前 4 个参数用寄存器(R0-R3),超出的压栈(右→左)
-
x86 全部压栈(左→右)
-
栈增长方向:
- ARM:满递减栈(FD, Full Descending)
- 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;
}
性能优化实践
- 栈大小计算:
- 静态分析:通过
arm-none-eabi-size查看.stack段 -
动态检测:填充魔术字(0xDEADBEEF),运行时检查是否被改写
-
结构体传参优化:
// 避免这样传参(会拷贝到栈上)void draw_big_struct(struct widget w); // 改为指针传递 void draw_big_struct(const struct widget *w);
安全防护方案
- 多任务栈隔离:
- RTOS 中每个任务独立栈空间
-
MPU 设置栈区域的读写权限
-
防栈溢出攻击:
# 禁止栈执行 arm-linux-gnueabihf-gcc -z noexecstack # 启用栈金丝雀 -fstack-protector-all
思考与进阶
- 中断优先级切换时,如何设计栈共享方案避免内存浪费?
- Cortex- M 的自动压栈机制与 A 系列的手动保存有何本质区别?
- 如何用 MPU 在 RTOS 中实现精确到 128 字节的栈边界保护?
通过 GDB 观察栈变化是最直观的学习方式,尝试单步执行以下命令:
maint print stack
frame
info registers sp lr pc
正文完
