揭秘 main() 之前的世界:Cortex-M 启动代码全解析
当我们写下 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 | __Vectors DCD __initial_sp ; Top of Stack |
DCD 指令类似于 C 语言的数组定义,它只是单纯地把数据填入 Flash。
2. 复位处理函数:Reset_Handler
CPU 读取到 PC 指针的初值后,跳转到 Reset_Handler。这是程序真正开始“动”的地方。
阶段一:设置栈指针 (Set Stack Pointer)
虽然硬件会自动加载 SP,但有些启动代码为了保险会再设置一次。
1 | LDR R0, =__initial_sp |
阶段二:数据搬运 (Copy Data)
这是最关键的一步。
- RO-Data (Read Only): 代码和常量,存放在 Flash 中。
- RW-Data (Read Write): 已初始化的全局变量(如
int g_cnt = 10;)。它们在 Flash 中保存了初值10,但运行时需要在 RAM 中被修改。
启动代码必须把 RW 段的初值从 Flash 拷贝到 RAM。
1 | ; 伪代码逻辑 |
如果少了这一步,你的全局变量初值就是随机数。
阶段三:BSS 清零 (Zero BSS)
- ZI-Data (Zero Initialized): 未初始化的全局变量(如
int g_buf[1024];)。C 标准规定它们初值必须为 0。
启动代码负责把 RAM 中 BSS 段对应的区域写 0。
1 | ; 伪代码逻辑 |
这就是为什么大数组可以放心不写初始化代码也不会是乱码的原因。
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 | BL 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