揭秘 main() 之前的世界:Cortex-M 启动代码全解析

Evek Golden Lv4

当我们写下 int main(void) { ... } 时,我们往往理所当然地认为这就是程序的起点。但在嵌入式世界里,main 之前发生的事情,才是区分“C 程序员”和“嵌入式工程师”的分水岭。

本文将以 Cortex-M3/M4 为例,逐行撕开启动文件 (startup_stm32.s) 的神秘面纱。

1. 也是第一行代码:中断向量表 (Vector Table)

MCU 上电复位后,硬件会做的第一件事,不是去跑什么 Reset Handler,而是去读取内存地址 0x00000000 和 0x00000004

  • 0x00000000: 存放栈顶地址 (Initial SP)。
  • 0x00000004: 存放复位中断服务函数 (Reset Handler) 的入口地址。

所以在汇编文件的最开头,我们总能看到这样一个表:

1
2
3
4
5
__Vectors       DCD     __initial_sp               ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
; ... 后续是各种外设中断

DCD 指令类似于 C 语言的数组定义,它只是单纯地把数据填入 Flash。

2. 复位处理函数:Reset_Handler

CPU 读取到 PC 指针的初值后,跳转到 Reset_Handler。这是程序真正开始“动”的地方。

阶段一:设置栈指针 (Set Stack Pointer)

虽然硬件会自动加载 SP,但有些启动代码为了保险会再设置一次。

1
2
LDR     R0, =__initial_sp
MOV SP, R0

阶段二:数据搬运 (Copy Data)

这是最关键的一步。

  • RO-Data (Read Only): 代码和常量,存放在 Flash 中。
  • RW-Data (Read Write): 已初始化的全局变量(如 int g_cnt = 10;)。它们在 Flash 中保存了初值 10,但运行时需要在 RAM 中被修改。

启动代码必须把 RW 段的初值从 Flash 拷贝到 RAM

1
2
3
4
5
6
7
8
9
10
11
; 伪代码逻辑
LDR R1, =_sidata ; Flash源地址
LDR R2, =_sdata ; RAM目标地址
LDR R3, =_edata ; 结束地址

CopyLoop:
CMP R2, R3
BGE ZeroBss
LDR R0, [R1], #4
STR R0, [R2], #4
B CopyLoop

如果少了这一步,你的全局变量初值就是随机数。

阶段三:BSS 清零 (Zero BSS)

  • ZI-Data (Zero Initialized): 未初始化的全局变量(如 int g_buf[1024];)。C 标准规定它们初值必须为 0。

启动代码负责把 RAM 中 BSS 段对应的区域写 0

1
2
3
4
5
6
7
8
9
10
; 伪代码逻辑
LDR R2, =_sbss ; BSS开始地址
LDR R3, =_ebss ; BSS结束地址
MOV R0, #0

ZeroLoop:
CMP R2, R3
BGE SystemInit
STR R0, [R2], #4
B ZeroLoop

这就是为什么大数组可以放心不写初始化代码也不会是乱码的原因。

3. 系统初始化:SystemInit

在进入 main 之前,通常还会调用厂商提供的 SystemInit 函数。

  • 目的:配置时钟树(PLL)、FPU 设置、Flash 等待周期 (Latency)。
  • 原因:main 函数第一行可能就想全速运行,所以时钟必须先备好。
1
BL  SystemInit

4. C++ 支持:__libc_init_array

如果你的项目用了 C++,你是如何在 main 之前执行全局对象的构造函数的?
秘密就在这里。标准库会遍历 .init_array 段,依次调用构造函数。

1
BL  __libc_init_array

5. 终于:Jump to Main

万事俱备,跳转。

1
2
BL  main
BX LR ; 理论上 main 不应该返回,如果返回就死循环

总结

启动代码不仅仅是板级支持包的一部分,它揭示了 C 语言运行环境(Runtime Environment)是如何被构建出来的。理解它,你就能理解链接脚本(Linker Script)的作用,也能明白为什么嵌入式程序的 RAM 和 Flash 使用量是那样计算的。

  • Title: 揭秘 main() 之前的世界:Cortex-M 启动代码全解析
  • Author: Evek Golden
  • Created at : 2025-01-05 23:11:00
  • Updated at : 2026-06-12 08:57:02
  • Link: https://blog.cocodemo.uno/posts/startup9x1/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments