单片机栈溢出三种场景详解

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)
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐