单片机栈溢出三种场景详解
·
文章目录
单片机栈溢出三种场景详解
1. 概述
本文分析三种常见的栈溢出场景,通过具体的代码示例和汇编分析,展示栈溢出如何导致程序异常。
共同点:
- 都是同一个函数
buf_overflow - 都是因为数组越界写入
- 通过改变参数
n的值,产生不同的破坏效果
函数原型:
void buf_overflow(int n, char val)
{
volatile char buf[5]; // 5字节的局部数组
volatile int a = 0x55; // 紧挨着 buf 的局部变量
buf[0] = 0x5a; // 写入 buf[0]
a++; // a = 0x56
buf[n] = val; // 越界写入!
myput_s_hex("buf[0] = ", buf[0]);
myputs("\n");
myput_s_hex("a is ", a);
myputs("\n");
}
栈帧布局(编译器分配):
┌─────────────────────────────────────────────────────────────────────────┐
│ buf_overflow 函数栈帧结构 │
└─────────────────────────────────────────────────────────────────────────┘
高地址
┌─────────────────┐
│ 保存的 lr │ SP+12 (返回地址)
├─────────────────┤
│ 保存的 r3 │ SP+8
├─────────────────┤
│ 保存的 r2 │ SP+4
├─────────────────┤
│ 保存的 r1 │ SP+0
├─────────────────┤ ← SP (栈指针)
│ │
低地址
注意:编译器将 buf[5] 和 int a 分配在栈帧中
实际布局取决于编译器的实现
2. 场景一:局部数组越界破坏其他变量
2.1 调用参数
buf_overflow(8, 'A'); // n=8, val='A'(0x41)
2.2 汇编代码
main:
0x08001f70: 2141 MOVS r1,#0x41 ; r1 = 'A'
0x08001f72: 2008 MOVS r0,#8 ; r0 = 8
0x08001f74: f7ffff62 BL buf_overflow ; 调用
buf_overflow:
0x08001e3c: b50e PUSH {r1-r3,lr} ; 保存寄存器
0x08001e3e: 225a MOVS r2,#0x5a ; r2 = 0x5a
0x08001e40: f88d2000 STRB r2,[sp,#0] ; buf[0] = 0x5a
0x08001e44: 2256 MOVS r2,#0x56 ; r2 = 0x56
0x08001e46: 9202 STR r2,[sp,#8] ; a = 0x56
0x08001e48: f80d1000 STRB r1,[sp,r0] ; buf[8] = 0x41 ← 越界!
0x08001e4c: f89d1000 LDRB r1,[sp,#0] ; 读取 buf[0]
0x08001e50: a006 ADR r0,{pc}+0x1c ; 字符串地址
0x08001e52: f000f8df BL myput_s_hex ; 打印 buf[0]
0x08001e56: a008 ADR r0,{pc}+0x22 ; 换行符
0x08001e58: f000f906 BL myputs
0x08001e5c: a007 ADR r0,{pc}+0x20 ; 字符串地址
0x08001e5e: 9902 LDR r1,[sp,#8] ; 读取 a
0x08001e60: f000f8d8 BL myput_s_hex ; 打印 a
0x08001e64: a004 ADR r0,{pc}+0x14 ; 换行符
0x08001e66: f000f8ff BL myputs
0x08001e6a: bd0e POP {r1-r3,pc} ; 返回
2.3 栈布局分析
假设栈起始地址 SP = 0x2000FFF0:
PUSH {r1-r3,lr} 后的栈布局:
┌─────────────────────────────────────────────────────────────────────────┐
│ 地址 内容 说明 │
├─────────────────────────────────────────────────────────────────────────┤
│ 0x2000FFF0 [r1] = ? SP+0 (buf[0] 的位置) │
│ 0x2000FFF4 [r2] = ? SP+4 (buf[4] 的位置) │
│ 0x2000FFF8 [r3] = ? SP+8 (int a 的位置) │
│ 0x2000FFFC [lr] = 0x08001F78 SP+12 (返回地址) │
└─────────────────────────────────────────────────────────────────────────┘
执行过程:
步骤1: STRB r2,[sp,#0]
将 0x5a 写入 SP+0
┌─────────────────────────────────────────┐
│ 0x2000FFF0: 5A 00 00 00 ← buf[0]=0x5a │
└─────────────────────────────────────────┘
步骤2: STR r2,[sp,#8]
将 0x56 写入 SP+8
┌─────────────────────────────────────────┐
│ 0x2000FFF8: 56 00 00 00 ← a=0x56 │
└─────────────────────────────────────────┘
步骤3: STRB r1,[sp,r0] (r0=8, r1=0x41)
将 0x41 写入 SP+8 的低字节
目标地址 = SP + 8 = 0x2000FFF8
┌─────────────────────────────────────────┐
│ 0x2000FFF8: 41 00 00 00 ← a 被破坏! │
└─────────────────────────────────────────┘
2.4 破坏结果
┌─────────────────────────────────────────────────────────────────────────┐
│ 破坏前后对比 │
├─────────────────────────────────────────────────────────────────────────┤
│ 变量 地址 破坏前 破坏后 说明 │
├─────────────────────────────────────────────────────────────────────────┤
│ buf[0] SP+0 0x5a 0x5a 未受影响 │
│ buf[4] SP+4 ? ? 未受影响 │
│ a SP+8 0x56 0x41 被破坏! │
│ lr SP+12 0x08001F78 0x08001F78 未受影响 │
└─────────────────────────────────────────────────────────────────────────┘
输出结果:
buf[0] = 0x5A
a is 0x41 ← 预期是 0x56,实际是 0x41
2.5 为什么 n=8 会破坏 a?
┌─────────────────────────────────────────────────────────────────────────┐
│ 地址偏移计算 │
└─────────────────────────────────────────────────────────────────────────┘
写入位置 = SP + n = SP + 8
栈布局:
SP+0: buf[0] (1字节)
SP+1: buf[1] (1字节)
SP+2: buf[2] (1字节)
SP+3: buf[3] (1字节)
SP+4: buf[4] (1字节)
SP+5: 填充 (编译器可能添加的对齐填充)
SP+6: 填充
SP+7: 填充
SP+8: int a (4字节) ← 写入位置!
结论:buf[8] 和变量 a 共享同一个地址
写入 buf[8] 实际上修改了 a 的值
3. 场景二:局部数组越界破坏返回地址
3.1 调用参数
buf_overflow(12, 'C'); // n=12, val='C'(0x43)
3.2 汇编代码
main:
0x08001f70: 2143 MOVS r1,#0x43 ; r1 = 'C'
0x08001f72: 200c MOVS r0,#12 ; r0 = 12
0x08001f74: f7ffff62 BL buf_overflow ; 调用
buf_overflow:
0x08001e3c: b50e PUSH {r1-r3,lr} ; 保存寄存器
0x08001e3e: 225a MOVS r2,#0x5a ; r2 = 0x5a
0x08001e40: f88d2000 STRB r2,[sp,#0] ; buf[0] = 0x5a
0x08001e44: 2256 MOVS r2,#0x56 ; r2 = 0x56
0x08001e46: 9202 STR r2,[sp,#8] ; a = 0x56
0x08001e48: f80d1000 STRB r1,[sp,r0] ; buf[12] = 0x43 ← 越界!
0x08001e4c: f89d1000 LDRB r1,[sp,#0] ; 读取 buf[0]
; ... 后续打印代码 ...
0x08001e6a: bd0e POP {r1-r3,pc} ; 返回 ← PC被破坏!
3.3 栈布局分析
假设栈起始地址 SP = 0x2000FFF0:
PUSH {r1-r3,lr} 后的栈布局:
┌─────────────────────────────────────────────────────────────────────────┐
│ 地址 内容 说明 │
├─────────────────────────────────────────────────────────────────────────┤
│ 0x2000FFF0 [r1] = 0x43 SP+0 (buf[0] 的位置) │
│ 0x2000FFF4 [r2] = ? SP+4 (buf[4] 的位置) │
│ 0x2000FFF8 [r3] = ? SP+8 (int a 的位置) │
│ 0x2000FFFC [lr] = 0x08001F78 SP+12 (返回地址) ← 目标位置! │
└─────────────────────────────────────────────────────────────────────────┘
执行过程:
步骤1: STRB r2,[sp,#0]
将 0x5a 写入 SP+0
┌─────────────────────────────────────────┐
│ 0x2000FFF0: 5A 00 00 00 ← buf[0]=0x5a │
└─────────────────────────────────────────┘
步骤2: STR r2,[sp,#8]
将 0x56 写入 SP+8
┌─────────────────────────────────────────┐
│ 0x2000FFF8: 56 00 00 00 ← a=0x56 │
└─────────────────────────────────────────┘
步骤3: STRB r1,[sp,r0] (r0=12, r1=0x43)
将 0x43 写入 SP+12 的低字节
目标地址 = SP + 12 = 0x2000FFFC
┌─────────────────────────────────────────┐
│ 0x2000FFFC: 43 1F 00 08 ← lr 被破坏! │
│ 原本是: 78 1F 00 08 (0x08001F78) │
└─────────────────────────────────────────┘
3.4 返回地址破坏详解
lr 寄存器的原始值 0x08001F78:
小端模式存储:
地址 字节 值
0x2000FFFC [0] 0x78 ← 低字节
0x2000FFFD [1] 0x1F
0x2000FFFE [2] 0x00
0x2000FFFF [3] 0x08 ← 高字节
STRB 只修改一个字节:
写入 0x43 到 0x2000FFFC:
地址 修改前 修改后
0x2000FFFC 0x78 0x43 ← 被修改!
0x2000FFFD 0x1F 0x1F (不变)
0x2000FFFE 0x00 0x00 (不变)
0x2000FFFF 0x08 0x08 (不变)
组合后:0x08001F78 → 0x08001F43
3.5 POP 指令执行后的效果
POP {r1-r3,pc}
┌─────────────────────────────────────────────────────────────────────────┐
│ POP 指令执行结果 │
├─────────────────────────────────────────────────────────────────────────┤
│ 寄存器 加载值 说明 │
├─────────────────────────────────────────────────────────────────────────┤
│ r1 0x5A 从 SP+0 加载 │
│ r2 ? 从 SP+4 加载 │
│ r3 0x56 从 SP+8 加载 │
│ pc 0x08001F42 从 SP+12 加载,硬件自动对齐 (bit[0]=0) │
└─────────────────────────────────────────────────────────────────────────┘
注意:加载的值是 0x08001F43,但 PC 硬件会自动将 bit[0] 清零
所以实际 PC = 0x08001F42

3.6 后续执行流程
┌─────────────────────────────────────────────────────────────────────────┐
│ PC = 0x08001F42 后的执行流程 │
└─────────────────────────────────────────────────────────────────────────┘
0x08001F42: BL OLED_Clear ← 被意外执行!
│
▼
OLED_Clear 函数执行
│
▼
返回到 0x08001F46
│
▼
0x08001F46: MOVS r1, #0 ← 继续执行错误的代码路径
│
▼
后续代码...
(导致一直死循环)

4. 场景三:打印结果不一致
4.1 调用参数
buf_overflow(8, 'C'); // n=8, val='C'(0x43)
4.2 汇编代码
main:
0x08001f70: 2141 MOVS r1,#0x41 ; r1 = 'A' (注意:这里是0x41)
0x08001f72: 2008 MOVS r0,#8 ; r0 = 8
0x08001f74: f7ffff62 BL buf_overflow ; 调用
buf_overflow:
0x08001e3c: b50e PUSH {r1-r3,lr} ; 保存寄存器
0x08001e3e: 225a MOVS r2,#0x5a ; r2 = 0x5a
0x08001e40: f88d2000 STRB r2,[sp,#0] ; buf[0] = 0x5a
0x08001e44: 2256 MOVS r2,#0x56 ; r2 = 0x56
0x08001e46: 9202 STR r2,[sp,#8] ; a = 0x56
0x08001e48: f80d1000 STRB r1,[sp,r0] ; buf[8] = 0x41 ← 越界!
0x08001e4c: 4611 MOV r1,r2 ; r1 = r2 = 0x56
0x08001e4e: a009 ADR r0,{pc}+0x26 ; "first: a is "
0x08001e50: f000f8f0 BL myput_s_hex ; 打印 a
0x08001e54: f89d1000 LDRB r1,[sp,#0] ; 读取 buf[0]
0x08001e58: a00a ADR r0,{pc}+0x2c ; "buf[0] = "
0x08001e5a: f000f8eb BL myput_s_hex ; 打印 buf[0]
0x08001e5e: a00c ADR r0,{pc}+0x32 ; 换行符
0x08001e60: f000f912 BL myputs
0x08001e64: a00b ADR r0,{pc}+0x30 ; "second: a is "
0x08001e66: 9902 LDR r1,[sp,#8] ; 读取 a
0x08001e68: f000f8e4 BL myput_s_hex ; 打印 a
0x08001e6c: a008 ADR r0,{pc}+0x24 ; 换行符
0x08001e6e: f000f90b BL myputs
0x08001e72: bd0e POP {r1-r3,pc} ; 返回
4.3 栈布局分析
PUSH {r1-r3,lr} 后的栈布局:
┌─────────────────────────────────────────────────────────────────────────┐
│ 地址 内容 说明 │
├─────────────────────────────────────────────────────────────────────────┤
│ 0x2000FFF0 [r1] = 0x41 SP+0 (buf[0] 的位置) │
│ 0x2000FFF4 [r2] = ? SP+4 (buf[4] 的位置) │
│ 0x2000FFF8 [r3] = ? SP+8 (int a 的位置) │
│ 0x2000FFFC [lr] = 0x08001F78 SP+12 (返回地址) │
└─────────────────────────────────────────────────────────────────────────┘
4.4 执行过程分析
步骤1: STRB r2,[sp,#0]
buf[0] = 0x5a
步骤2: STR r2,[sp,#8]
a = 0x56
步骤3: STRB r1,[sp,r0] (r0=8, r1=0x41)
buf[8] = 0x41
→ 实际上修改了 a 的低字节!
→ a 从 0x00000056 变成 0x00000041
步骤4: MOV r1, r2
r1 = r2 = 0x56
→ 注意:这里是从寄存器读取,不是从栈上读取!
步骤5: 第一次打印 "first: a is "
打印的是 r1 = 0x56 (从寄存器读取)
步骤6: LDRB r1,[sp,#0]
从栈上读取 buf[0] = 0x5a
步骤7: 打印 "buf[0] = 0x5A"
步骤8: 第二次打印 "second: a is "
LDR r1,[sp,#8]
从栈上读取 a = 0x41 (被破坏后的值)
打印的是 0x41
4.5 打印结果不一致的原因
┌─────────────────────────────────────────────────────────────────────────┐
│ 打印结果对比 │
├─────────────────────────────────────────────────────────────────────────┤
│ 打印内容 实际值 原因 │
├─────────────────────────────────────────────────────────────────────────┤
│ first: a is 0x56 从寄存器 r1 读取 (MOV r1, r2) │
│ buf[0] = 0x5A 从栈上读取,未被破坏 │
│ second: a is 0x41 从栈上读取,已被破坏 │
└─────────────────────────────────────────────────────────────────────────┘
关键点:
- 第一次打印 a 时,使用的是寄存器中的值 (0x56)
- 第二次打印 a 时,从栈上重新加载,得到被破坏的值 (0x41)
- 这就是为什么两次打印结果不一致!
4.6 编译器优化的影响
┌─────────────────────────────────────────────────────────────────────────┐
│ 编译器行为分析 │
└─────────────────────────────────────────────────────────────────────────┘
C代码:
volatile int a = 0x55;
a++; // a = 0x56
buf[n] = val; // 可能破坏 a
myput_s_hex("first: a is ", a); // 打印 a
编译器生成的汇编:
MOVS r2, #0x56 // r2 = 0x56
STR r2, [sp, #8] // 将 0x56 存入栈上的 a
STRB r1, [sp, r0] // buf[n] = val (可能破坏 a)
MOV r1, r2 // r1 = r2 = 0x56 (从寄存器读取)
BL myput_s_hex // 打印 r1 (0x56)
问题:
- 编译器将 a 的值缓存在寄存器 r2 中
- 第一次打印时直接使用寄存器值,没有从栈上重新加载
- 所以打印的是被破坏前的值 (0x56)
为什么第二次打印不同:
LDR r1, [sp, #8] // 从栈上重新加载 a
BL myput_s_hex // 打印 r1 (0x41)
- 第二次打印时,编译器生成了从栈上加载的指令
- 所以打印的是被破坏后的值 (0x41)
更多推荐


所有评论(0)