共计 1546 个字符,预计需要花费 4 分钟才能阅读完成。
背景痛点:那些年我们踩过的栈坑
刚接触 ARM 开发时,我最常遇到的崩溃就是 栈溢出。比如在实现递归算法时,没有正确估算栈深度,结果程序跑着跑着就神秘崩溃。更棘手的是中断上下文保存不完整——有一次我在中断服务函数里调用了其他函数,但忘记保存 LR 寄存器,导致程序返回时直接飞到了未知地址。

// 典型栈溢出示例
void recursive_func(int n) {char local_buf[256]; // 每次递归都占用栈空间
if (n > 0) recursive_func(n-1); // 递归过深就会爆栈
}
ARM vs x86:关键差异速览
和 x86 不同,ARM 架构的函数调用遵循AAPCS 标准(ARM Architecture Procedure Call Standard),有几个重要特性:
- 寄存器分工明确:
- r0-r3 用于参数传递和临时值
- r4-r11 必须由被调函数保存
-
r13(SP) 是栈指针,r14(LR)存返回地址
-
栈操作方向:
- ARM 使用 满递减栈(FD),新数据存入更低地址
-
x86 默认采用 满递增栈(FA)
-
返回地址处理:
- x86 通过 call 指令自动压栈
- ARM 需要手动处理 LR 寄存器
手把手汇编实战
叶子函数示例(不调用其他函数)
@ ARMv7 示例
global leaf_func
leaf_func:
@ 不需要保存 LR,因为不调用其他函数
str r4, [sp, #-4]! @ 保存 r4 到栈(注意! 会更新 SP)... 函数逻辑 ...
ldr r4, [sp], #4 @ 恢复 r4
bx lr @ 直接返回
非叶子函数示例
@ ARMv8 示例
non_leaf:
stp x29, x30, [sp, #-16]! @ 保存 FP 和 LR
mov x29, sp @ 设置帧指针
... 可能调用其他函数 ...
ldp x29, x30, [sp], #16 @ 恢复 FP 和 LR
ret @ 等价于 bx lr
关键点:
– STMFD(Store Multiple Full Descending)指令可以批量保存寄存器
– ARMv8 开始推荐使用 stp/ldp 替代stm/ldm
– 栈操作后加 ! 才能自动更新 SP
三大避坑指南
1. 栈内存对齐
ARM 要求 8 字节对齐(ARMv7)或16 字节对齐(ARMv8)。未对齐会导致:
- 性能下降(需要额外对齐操作)
- 某些 NEON 指令直接崩溃
解决方法:
void aligned_func() {__attribute__((aligned(16))) char buf[128];
// 或者使用编译器选项 -mstack-alignment=16
}
2. 中断嵌套保护
中断发生时硬件会自动保存部分寄存器,但软件需要:
- 保存剩余通用寄存器
- 特别注意保存 LR 寄存器
- 使用专用中断栈(避免污染线程栈)
3. GDB 调试技巧
(gdb) bt full # 查看完整调用栈
(gdb) x/20x $sp # 查看栈内存内容
(gdb) info frame # 分析当前栈帧结构
进阶:反编译观察
用 objdump 看编译器生成的代码:
arm-linux-gnueabihf-objdump -d a.out | less
你会注意到:
– 编译器会自动插入栈保护代码(如 -fstack-protector)
– 优化级别不同,栈操作差异很大(-O0 会多很多冗余操作)
实验环境搭建
用 QEMU 快速验证:
# 安装 ARM 模拟环境
sudo apt install qemu-user-static gcc-arm-linux-gnueabihf
# 编译测试
arm-linux-gnueabihf-gcc -g test.c -o test
# 运行调试
qemu-arm -g 1234 test &
gdb-multiarch -ex "target remote localhost:1234"
通过这篇文章,希望能帮你避开我当年踩过的那些坑。ARM 栈操作看似简单,但细节决定成败。建议多写测试代码,用 GDB 单步跟踪观察 SP 和 LR 的变化,这才是最好的学习方法。
正文完
