STM32启动机理深度解析:从链接脚本到微机原理

次浏览

概述

当我们按下复位键或上电后,STM32是如何从"死寂"的状态开始执行代码,最终运行到我们熟悉的 main() 函数的?这个过程涉及链接脚本启动文件C运行环境以及微机原理等多个层面的知识。

本文将深入剖析STM32的启动机理,从底层硬件到上层软件,完整揭示这一过程。


一、STM32内存映射与启动原理

1.1 STM32内存映射

STM32使用统一的地址空间映射,不同类型的存储器映射到不同的地址区域:

1
2
3
4
5
6
7
8
地址范围              大小        存储器
─────────────────────────────────────────────────────
0x0000 0000 - 0x1FFF FFFF   512MB   Code区域 (Flash/SRAM重映射)
0x2000 0000 - 0x3FFF FFFF   512MB   SRAM区域
0x4000 0000 - 0x5FFF FFFF   512MB   外设区域
0x6000 0000 - 0x9FFF FFFF   1GB     外部RAM
0xA000 0000 - 0xDFFF FFFF   1GB     外部设备
0xE000 0000 - 0xFFFF FFFF   512MB   系统区域 (Cortex-M内核)

1.2 启动模式选择

STM32通过 BOOT0 和 BOOT1 引脚决定启动时的存储器映射:

BOOT1 BOOT0 启动模式 0x00000000 映射到
X 0 主Flash启动 Flash (0x08000000)
0 1 系统存储器启动 System Memory (Bootloader)
1 1 SRAM启动 SRAM (0x20000000)

1.3 启动时的地址重映射

当选择从Flash启动时,CPU看到的地址映射:

1
2
3
4
5
6
7
CPU视角                    物理存储器
──────────────────────────────────────────
0x0000 0000 ───────────► Flash起始地址 (0x0800 0000)
                    │  这里存放中断向量表
0x0800 0000 ───────────► Flash物理起始

关键原理:Cortex-M 内核复位后从地址 0x00000000 读取栈指针(SP)初值,从 0x00000004 读取复位向量(PC)初值


二、链接脚本(Linker Script)深度解析

链接脚本(.ld文件)控制程序的内存布局,决定代码和数据在存储器中的位置。

2.1 链接脚本的基本结构

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/* STM32F103C8T6 典型链接脚本 */

/* 1. 入口点定义 */
ENTRY(Reset_Handler)

/* 2. 内存区域定义 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
    RAM (rwx)   : ORIGIN = 0x20000000, LENGTH = 20K
}

/* 3. 栈大小定义 */
_stack_size = 0x400;  /* 1KB 栈 */

/* 4. 段布局 */
SECTIONS
{
    /* 中断向量表 - 必须放在Flash起始位置 */
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))
        . = ALIGN(4);
    } >FLASH

    /* 代码段 */
    .text :
    {
        . = ALIGN(4);
        *(.text)           /* .text sections (code) */
        *(.text*)          /* .text* sections (code) */
        *(.glue_7)         /* ARM/Thumb glue code */
        *(.glue_7t)
        *(.eh_frame)
        
        KEEP(*(.init))
        KEEP(*(.fini))
        
        . = ALIGN(4);
        _etext = .;        /* 代码段结束地址 */
    } >FLASH

    /* 只读数据段 */
    .rodata :
    {
        . = ALIGN(4);
        *(.rodata)
        *(.rodata*)
        . = ALIGN(4);
    } >FLASH

    /* ARM特定属性 */
    .ARM.extab :
    {
        *(.ARM.extab* .gnu.linkonce.armextab.*)
    } >FLASH

    .ARM :
    {
        __exidx_start = .;
        *(.ARM.exidx*)
        __exidx_end = .;
    } >FLASH

    /* 用于启动文件中数据拷贝的符号 */
    _sidata = LOADADDR(.data);

    /* 已初始化数据段 - 存放在RAM,启动时从Flash拷贝 */
    .data :
    {
        . = ALIGN(4);
        _sdata = .;        /* 数据段起始地址 (RAM中) */
        *(.data)
        *(.data*)
        . = ALIGN(4);
        _edata = .;        /* 数据段结束地址 */
    } >RAM AT> FLASH       /* 运行在RAM,加载时在FLASH */

    /* 未初始化数据段 (BSS) */
    .bss :
    {
        . = ALIGN(4);
        _sbss = .;         /* BSS段起始地址 */
        __bss_start__ = _sbss;
        *(.bss)
        *(.bss*)
        *(COMMON)
        . = ALIGN(4);
        _ebss = .;         /* BSS段结束地址 */
        __bss_end__ = _ebss;
    } >RAM

    /* 用户堆栈定义 */
    ._user_heap_stack :
    {
        . = ALIGN(8);
        PROVIDE(end = .);
        PROVIDE(_end = .);
        . = . + _min_heap_size;
        . = . + _min_stack_size;
        . = ALIGN(8);
    } >RAM

    /* 丢弃不需要的段 */
    /DISCARD/ :
    {
        libc.a(*)
        libm.a(*)
        libgcc.a(*)
    }

    /* 提供栈顶地址 */
    _estack = ORIGIN(RAM) + LENGTH(RAM);
}

2.2 关键符号解析

链接脚本定义的符号在启动代码中使用:

1
2
3
4
5
6
7
// 这些符号由链接器自动计算
extern uint32_t _estack;    // 栈顶地址 (RAM末端)
extern uint32_t _sidata;    // 数据段在Flash中的加载地址
extern uint32_t _sdata;     // 数据段在RAM中的起始地址
extern uint32_t _edata;     // 数据段在RAM中的结束地址
extern uint32_t _sbss;      // BSS段起始地址
extern uint32_t _ebss;      // BSS段结束地址

2.3 AT关键字的作用

1
.data : { ... } >RAM AT> FLASH

这表示:

  • 运行地址(VMA):在RAM中(变量实际运行时的位置)
  • 加载地址(LMA):在FLASH中(程序烧录时的位置)

启动代码需要将数据从LMA拷贝到VMA。

2.4 内存布局图示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Flash (0x0800 0000)                    RAM (0x2000 0000)
┌─────────────────────┐               ┌─────────────────────┐
│ .isr_vector (向量表) │               │                     │
├─────────────────────┤               │   栈 (向下增长)      │
│ .text (代码)         │               │         ↓           │
│                     │               ├─────────────────────┤ ← _estack
│                     │               │                     │
├─────────────────────┤               │   堆 (向上增长)      │
│ .rodata (只读数据)   │               │         ↑           │
│                     │               ├─────────────────────┤ ← _ebss
├─────────────────────┤ ← _sidata     │ .bss (未初始化数据) │
│ .data (初始值镜像)   │               │                     │
│                     │               ├─────────────────────┤ ← _sbss
└─────────────────────┘               │ .data (已初始化数据)│
                                      │                     │
                                      ├─────────────────────┤ ← _edata
                                      │                     │ ← _sdata
                                      └─────────────────────┘

三、启动文件(startup.s)详解

启动文件是用汇编编写的,负责处理器复位后的初始化工作。

3.1 完整启动文件分析

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
 * STM32F103 启动文件
 * 文件: startup_stm32f103xb.s
 */

.syntax unified
.cpu cortex-m3
.fpu softvfp
.thumb

/* =============== 全局符号定义 =============== */
.global Reset_Handler
.global Default_Handler

/* 导出符号供C代码使用 */
.global _estack           /* 栈顶,由链接脚本提供 */

/* =============== 中断向量表 =============== */
.section .isr_vector, "a", %progbits
.type vector_table, %object
.size vector_table, .-vector_table

vector_table:
    .word _estack           /* 0x00: 初始栈指针 (MSP) */
    .word Reset_Handler     /* 0x04: 复位向量 */
    .word NMI_Handler       /* 0x08: NMI */
    .word HardFault_Handler /* 0x0C: Hard Fault */
    .word MemManage_Handler /* 0x10: MPU Fault */
    .word BusFault_Handler  /* 0x14: Bus Fault */
    .word UsageFault_Handler/* 0x18: Usage Fault */
    .word 0                 /* 0x1C: Reserved */
    .word 0                 /* 0x20: Reserved */
    .word 0                 /* 0x24: Reserved */
    .word 0                 /* 0x28: Reserved */
    .word SVC_Handler       /* 0x2C: SVCall */
    .word DebugMon_Handler  /* 0x30: Debug Monitor */
    .word 0                 /* 0x34: Reserved */
    .word PendSV_Handler    /* 0x38: PendSV */
    .word SysTick_Handler   /* 0x3C: SysTick */

    /* 外部中断向量 */
    .word WWDG_IRQHandler   /* 0x40: Window Watchdog */
    .word PVD_IRQHandler    /* 0x44: PVD */
    .word TAMPER_IRQHandler /* 0x48: Tamper */
    .word RTC_IRQHandler    /* 0x4C: RTC */
    .word FLASH_IRQHandler  /* 0x50: Flash */
    .word RCC_IRQHandler    /* 0x54: RCC */
    .word EXTI0_IRQHandler  /* 0x58: EXTI Line 0 */
    /* ... 更多外部中断 ... */

/* =============== 复位处理程序 =============== */
.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function

Reset_Handler:
    /* 1. 设置栈指针(通常硬件已自动完成,但确保一下) */
    ldr r0, =_estack
    mov sp, r0

    /* 2. 将.data段从Flash拷贝到RAM */
    ldr r0, =_sdata       /* 目标地址: RAM中.data起始 */
    ldr r1, =_edata       /* 结束地址: RAM中.data结束 */
    ldr r2, =_sidata      /* 源地址: Flash中.data镜像 */

copy_data_loop:
    cmp r0, r1            /* 检查是否拷贝完毕 */
    bge copy_data_done
    ldr r3, [r2], #4      /* 从Flash读取一个字 */
    str r3, [r0], #4      /* 写入RAM */
    b copy_data_loop

copy_data_done:

    /* 3. 清零.bss段 */
    ldr r0, =_sbss        /* BSS起始地址 */
    ldr r1, =_ebss        /* BSS结束地址 */
    mov r2, #0            /* 清零值 */

zero_bss_loop:
    cmp r0, r1
    bge zero_bss_done
    str r2, [r0], #4      /* 写入0 */
    b zero_bss_loop

zero_bss_done:

    /* 4. 调用SystemInit(时钟初始化等) */
    bl SystemInit

    /* 5. 调用C库初始化(可选,用于静态变量构造等) */
    bl __main             /* ARM Compiler */
    /* 或 bl _start */    /* GCC */

    /* 不应该到达这里 */
    b .

.size Reset_Handler, .-Reset_Handler

/* =============== 默认中断处理程序 =============== */
.section .text.Default_Handler, "ax", %progbits
.type Default_Handler, %function

Default_Handler:
    b .                    /* 无限循环 */

.size Default_Handler, .-Default_Handler

/* =============== 弱符号中断处理程序 =============== */
/* 用户可以在C代码中重新定义这些函数 */
.weak NMI_Handler
.thumb_set NMI_Handler, Default_Handler

.weak HardFault_Handler
.thumb_set HardFault_Handler, Default_Handler

/* ... 其他中断的弱符号定义 ... */

3.2 向量表结构详解

Cortex-M 的向量表结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
偏移    异常类型              说明
────────────────────────────────────────────────
0x00    初始SP值             栈指针初始值
0x04    Reset                复位异常
0x08    NMI                  不可屏蔽中断
0x0C    HardFault            硬件错误
0x10    MemManage            内存管理错误
0x14    BusFault             总线错误
0x18    UsageFault           用法错误
0x1C-0x28 Reserved           保留
0x2C    SVCall               系统服务调用
0x30    DebugMonitor         调试监视器
0x34    Reserved             保留
0x38    PendSV               挂起系统服务
0x3C    SysTick              系统滴答定时器
0x40+   IRQ 0-N              外部中断

四、C运行环境初始化

4.1 为什么要初始化C运行环境?

C语言程序运行前必须满足:

  1. 栈已建立:局部变量、函数调用需要栈
  2. 全局变量已初始化
    • .data 段变量需要正确的初始值
    • .bss 段变量需要清零
  3. 堆可用malloc 需要堆空间
  4. 标准库可用printf 等函数依赖底层初始化

4.2 .data 段初始化原理

问题:全局初始化变量存储在哪里?

1
int global_var = 0x12345678;  // 已初始化全局变量

答案

  • 初始值存储在 Flash(.data 镜像)
  • 变量运行时在 RAM(.data 段)
  • 启动时需要拷贝
1
2
3
4
5
6
7
8
9
Flash                    RAM
┌──────────────┐        ┌──────────────┐
│ 0x12         │        │              │
│ 0x34         │ ───拷贝──► │ 0x12         │
│ 0x56         │        │ 0x34         │
│ 0x78         │        │ 0x56         │
└──────────────┘        │ 0x78         │
   _sidata              └──────────────┘
                           _sdata

4.3 .bss 段初始化原理

1
2
int uninit_var;          // 未初始化全局变量(BSS段)
static int static_var;   // 未初始化静态变量(BSS段)

C 标准规定未初始化的全局/静态变量必须初始化为 0。

启动代码需要将 BSS 段清零:

1
2
3
4
5
6
7
8
RAM
┌──────────────┐
│ 0x00         │
│ 0x00         │ ← 清零前可能是随机值
│ 0x00         │
│ 0x00         │
└──────────────┘
   _sbss ~ _ebss

4.4 SystemInit 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * @brief 系统时钟配置
 * @note 在启动文件中调用,在 main() 之前执行
 */
void SystemInit(void)
{
    /* 1. 复位 RCC 时钟配置 */
    RCC->CR |= RCC_CR_HSION;        /* 使能内部高速时钟 HSI */
    RCC->CFGR = 0x00000000;         /* 复位 CFGR 寄存器 */
    
    /* 2. 关闭所有中断 */
    RCC->CIR = 0x00000000;
    
    /* 3. 配置向量表位置 */
#ifdef VECT_TAB_SRAM
    SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
#else
    SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
#endif
    
    /* 4. 时钟初始化(通常在 SystemClock_Config 中完成) */
    /* ... */
}

五、从微机原理角度分析启动过程

5.1 处理器复位行为

当 STM32 复位时,Cortex-M 内核执行以下硬件操作:

1
2
3
4
5
6
7
1. 从地址 0x00000000 加载 MSP (主栈指针)
   MSP = *0x00000000
   
2. 从地址 0x00000004 加载 PC (程序计数器)
   PC = *0x00000004
   
3. 处理器开始执行 PC 指向的代码

5.2 为什么第一个值是栈指针?

微机原理:栈用于:

  • 保存函数返回地址
  • 保存局部变量
  • 保存中断上下文

Cortex-M 使用满递减栈

1
2
3
4
5
6
7
8
9
高地址
    ┌─────────────┐
    │             │
    │   栈空间    │ ← SP 初始值指向栈顶
    │             │
    │      ↓      │ ← 栈向下增长
    │             │
    └─────────────┘
低地址

5.3 中断向量表的硬件实现

Cortex-M 使用向量表而非中断跳转表

1
2
3
4
5
6
7
8
9
传统ARM7/ARM9:
┌─────────────┐
│ 跳转指令    │ ──跳转──► 中断服务程序
└─────────────┘

Cortex-M:
┌─────────────┐
│ 地址值      │ ──直接加载PC──► 中断服务程序
└─────────────┘

优势

  • 减少跳转开销
  • 支持动态修改向量表(VTOR寄存器)

5.4 模式切换与特权级别

Cortex-M 的执行模式:

模式 特权级别 使用场景
Thread 特权/非特权 普通程序执行
Handler 特权 中断/异常处理

启动时:

  1. 复位后处于 Thread 模式,特权级别
  2. 发生中断时切换到 Handler 模式
  3. 可通过 CONTROL 寄存器切换特权级别

5.5 启动时序图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
时间轴          CPU行为                    代码执行
────────────────────────────────────────────────────────
    │  上电/复位
    │  硬件加载 MSP ← *0x00000000
    │  硬件加载 PC  ← *0x00000004
    │  开始执行 Reset_Handler
    │  ├─ 设置栈(确保)
    │  ├─ 拷贝 .data
    │  ├─ 清零 .bss
    │  ├─ 调用 SystemInit
    │  └─ 调用 __main / _start
    │  C 运行环境初始化
    │  ├─ 初始化标准库
    │  ├─ 调用静态构造函数
    │  └─ 调用 main()
    │  用户程序执行

六、实际调试:查看启动过程

6.1 查看内存布局

使用 objdumpnm 工具:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 查看符号地址
arm-none-eabi-nm -n firmware.elf | head -20

# 输出示例:
# 08000000 T Reset_Handler
# 08000100 T main
# 08000200 D _sdata
# 08000210 D _edata
# 20000000 B _sbss
# 20005000 R _estack

# 查看段布局
arm-none-eabi-objdump -h firmware.elf

6.2 查看向量表

1
2
3
4
5
6
7
8
9
# 反汇编查看向量表
arm-none-eabi-objdump -d -j .isr_vector firmware.elf

# 输出示例:
# 08000000 <vector_table>:
#  8000000: 20005000    (栈指针)
#  8000004: 08000189    (Reset_Handler)
#  8000008: 0800018b    (NMI_Handler)
#  ...

6.3 GDB 调试启动过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 启动调试
arm-none-eabi-gdb firmware.elf

# 在 Reset_Handler 设置断点 break Reset_Handler

# 复位并运行 monitor reset
(gdb) continue

# 单步执行 stepi

# 查看寄存器 info registers
# 查看栈 pointer $sp

七、自举启动:最小可运行示例

7.1 不使用标准库的最小启动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* main.c - 裸机最小示例 */
#include <stdint.h>

/* 寄存器定义 */
#define RCC_BASE    0x40021000
#define GPIOC_BASE  0x40011000

#define RCC_APB2ENR (*(volatile uint32_t*)(RCC_BASE + 0x18))
#define GPIOC_CRH   (*(volatile uint32_t*)(GPIOC_BASE + 0x04))
#define GPIOC_ODR   (*(volatile uint32_t*)(GPIOC_BASE + 0x0C))

/* 简单延时 */
void delay(volatile uint32_t count) {
    while (count--);
}

/* 主函数 */
int main(void) {
    /* 使能 GPIOC 时钟 */
    RCC_APB2ENR |= (1 << 4);
    
    /* 配置 PC13 为推挽输出 */
    GPIOC_CRH &= ~(0x0F << 20);
    GPIOC_CRH |= (0x02 << 20);
    
    /* LED 闪烁 */
    while (1) {
        GPIOC_ODR ^= (1 << 13);  /* 翻转 PC13 */
        delay(500000);
    }
    
    return 0;
}

/* 启动代码(内联) */
__attribute__((naked)) void Reset_Handler(void) {
    /* 设置栈指针 */
    __asm volatile (
        "ldr r0, =_estack\n"
        "mov sp, r0\n"
    );
    
    /* 清零 BSS(简化版) */
    extern char _sbss, _ebss;
    char *p = &_sbss;
    while (p < &_ebss) *p++ = 0;
    
    /* 调用 main */
    main();
    
    /* 死循环 */
    while (1);
}

/* 向量表(放在特殊段) */
typedef void (*vector_t)(void);
__attribute__((section(".isr_vector")))
vector_t vector_table[] = {
    (vector_t)&_estack,    /* 初始栈指针 */
    Reset_Handler,         /* Reset */
    0, 0, 0, 0, 0, 0, 0,   /* 保留 */
    0, 0, 0, 0, 0, 0,      /* 保留 */
    /* ... 更多中断向量 ... */
};

7.2 对应的简化链接脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* minimal.ld */
ENTRY(Reset_Handler)

MEMORY {
    FLASH : ORIGIN = 0x08000000, LENGTH = 64K
    RAM   : ORIGIN = 0x20000000, LENGTH = 20K
}

_estack = ORIGIN(RAM) + LENGTH(RAM);
_sbss = ORIGIN(RAM);
_ebss = _sbss + 1K;

SECTIONS {
    .isr_vector : { *(.isr_vector) } > FLASH
    .text : { *(.text) *(.text*) } > FLASH
    .rodata : { *(.rodata) } > FLASH
    .bss : { *(.bss) } > RAM
}

八、常见问题与调试技巧

8.1 程序无法启动

检查清单

  1. 向量表位置:确保在 Flash 起始地址(0x08000000)
  2. 栈大小:检查是否栈溢出
  3. 时钟配置:SystemInit 是否正确配置
  4. 启动模式:BOOT0/BOOT1 引脚是否正确

8.2 HardFault 调试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* HardFault 处理函数,打印调试信息 */
void HardFault_Handler(void) {
    uint32_t *stack_ptr;
    
    /* 获取栈指针 */
    __asm volatile ("mov %0, sp" : "=r"(stack_ptr));
    
    /* 打印寄存器信息(需要串口支持) */
    printf("HardFault!\n");
    printf("R0  = 0x%08X\n", stack_ptr[0]);
    printf("R1  = 0x%08X\n", stack_ptr[1]);
    printf("R2  = 0x%08X\n", stack_ptr[2]);
    printf("R3  = 0x%08X\n", stack_ptr[3]);
    printf("R12 = 0x%08X\n", stack_ptr[4]);
    printf("LR  = 0x%08X\n", stack_ptr[5]);  /* 返回地址 */
    printf("PC  = 0x%08X\n", stack_ptr[6]);  /* 出错地址 */
    printf("xPSR= 0x%08X\n", stack_ptr[7]);
    
    while (1);
}

8.3 栈溢出检测

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 在链接脚本中定义栈区域 */
_stack_start = ORIGIN(RAM) + LENGTH(RAM) - _stack_size;

/* 启动时填充栈区域为特定值 */
void stack_canary_init(void) {
    extern uint32_t _stack_start;
    extern uint32_t _estack;
    uint32_t *ptr = &_stack_start;
    
    while (ptr < &_estack) {
        *ptr++ = 0xDEADBEEF;  /* 哨兵值 */
    }
}

/* 检查栈是否溢出 */
bool stack_overflow_check(void) {
    extern uint32_t _stack_start;
    return _stack_start != 0xDEADBEEF;
}

九、总结

9.1 启动流程全景图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────────────────────────────┐
│                      STM32 启动流程                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 上电/复位                                               │
│       │                                                     │
│       ▼                                                     │
│  2. 硬件: 加载 MSP ← *0x00000000                            │
│          加载 PC  ← *0x00000004                             │
│       │                                                     │
│       ▼                                                     │
│  3. Reset_Handler (启动文件)                                │
│       ├─ 设置栈指针                                         │
│       ├─ 拷贝 .data 段 (Flash → RAM)                        │
│       ├─ 清零 .bss 段                                       │
│       └─ 调用 SystemInit                                    │
│       │                                                     │
│       ▼                                                     │
│  4. C运行环境初始化                                         │
│       ├─ 初始化标准库                                       │
│       └─ 调用静态构造函数(C++)                              │
│       │                                                     │
│       ▼                                                     │
│  5. main()                                                  │
│       │                                                     │
│       ▼                                                     │
│  6. 应用程序运行                                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

9.2 关键文件职责

文件 职责
链接脚本(.ld) 定义内存布局,生成段地址符号
启动文件(.s) 初始化C环境,提供向量表
SystemInit() 时钟配置,向量表重定位
main() 用户程序入口

9.3 核心概念

  1. 向量表:存储异常/中断处理函数地址的表
  2. 链接脚本:控制代码和数据的内存布局
  3. C运行环境:全局变量初始化、栈建立、库初始化
  4. VMA vs LMA:运行地址 vs 加载地址

参考资料

  • 《ARM Cortex-M3 权威指南》
  • STM32 参考手册
  • GNU LD 手册
  • ARM Architecture Procedure Call Standard (AAPCS)
使用 Hugo 构建
主题 StackJimmy 设计