大目熊 · 9月13日 · 广东

MCU 启动流程

MCU 启动流程深度解析:从复位到 main,以及用 CRT 库接管初始化

关键词:MCU、启动文件、向量表、VTOR、.data、.bss、CRT、crt0、picolibc、STM32F103、链接脚本

如果你有 MCU 开发经验,应该对"启动文件"不陌生。MCU 程序从上电到 main 函数之间,有一段代码是必须提前跑完的——那就是启动文件(Startup File) 里的初始化流程。

本文将从 硬件复位行为 出发,层层拆解 MCU 的启动过程,并最终探讨如何用 CRT 库(C Runtime) 标准化地接管初始化工作。

完整视频流程:
【MCU C Runtime Bring-up】 https://www.bilibili.com/vide...


一、初始化的本质:两件事

启动阶段的初始化,本质上就两件事:

  1. 给 C 语言建立运行时环境
  2. 给 MCU 建立硬件运行环境

二、C 语言运行时环境的建立

为了让 C 代码能正常运行,启动阶段需要完成以下流程:

1. 初始化栈指针(SP)

C 函数调用需要栈。栈顶指针在复位时由硬件自动从向量表第一项加载到 SP——但前提是链接脚本提供了栈顶位置

2. 拷贝 .data 段(从 Flash 到 RAM)

程序中有初始值的全局或静态变量(.data section),其初始值存储在 Flash 中,但运行时必须位于 RAM。因此启动时需要把这些初始值从 Flash 拷贝到 RAM。

3. 清零 .bss 段

未初始化的全局或静态变量(.bss section),编译器只在链接时记录它的大小和位置,运行时需要将对应的 RAM 区域全部清零


三、MCU 硬件启动:复位行为

3.1 ARMv7-M 的复位流程

ARMv7-M 架构参考手册用伪代码描述了复位行为,主要包括:

  • 重置部分寄存器
  • 清除异常状态
  • 从向量表起始地址读取 4 字节 数据 → 即栈顶地址(向量表第一项)
  • 从向量表偏移 4 处读取 4 字节 → 即复位中断函数地址(中断号 1)
  • 跳转到复位中断函数执行

关键点:向量表的第一项是栈顶地址,第二项是复位中断处理函数地址

3.2 向量表为什么必须在 Flash 起始位置?

这里有一个关键问题:向量表是软件提供的,MCU 启动时怎么知道去哪里找?

芯片内核通过 VTOR 寄存器 获取向量表起始地址。ARMv7-M 规定:

VTOR 复位值为 0x00000000,所以复位瞬间内核固定从地址 0 取向量

但 MCU 程序烧录在 Flash 中,向量表也在 Flash 里,而 Flash 的起始映射地址通常并不是 0。那复位流程如何通过地址 0 访问到 Flash 内容?

3.3 STM32F103 的硬件解决方案

这个问题由芯片厂商解决。STM32F103 手册在"启动配置"中说明:

当从内部 Flash 启动时,内部 Flash 会被 alias(映射)到地址 0 的启动内存空间。

此时通过地址 0 或 Flash 的原始映射地址,都能访问到内部 Flash。因此,只要在 Flash 起始位置放置向量表,MCU 复位时就能通过地址 0 顺利取到向量表

总结复位流程:

上电/复位
  → 内核复位相关寄存器
  → 从地址 0(被 aliase 到 Flash)读取向量表
  → 取栈顶地址 → 加载到 SP
  → 取复位中断函数地址
  → 跳转到复位中断函数执行

四、进入复位中断函数后发生了什么

下面基于 STM32F103 工程来分析(CubeMX 生成)。

4.1 启动文件结构

第一步:定义复位中断函数(稍后分析其内部逻辑)。

第二步:定义中断向量表——

  • 第一项为栈顶地址(由链接文件提供)
  • 其后按中断号顺序排列各中断函数符号
  • 整个向量表被放到特定 section,链接文件会将该 section 置于 Flash 起始位置

这样硬件复位时才能顺利取到向量表。

4.2 复位中断函数内部流程

进入复位中断函数后,依次执行:

步骤操作说明
1调用 SystemInit设置 VTOR 寄存器,将向量表重定位到 Flash 原始地址(运行期按真实映射地址访问)
2拷贝 .data把 Flash 中已初始化的全局/静态变量值拷贝到 RAM
3清零 .bss将 RAM 中未初始化变量区域清零
4调用 __libc_init_array调用全局构造函数(C++ / __attribute__((constructor))
5跳转到 mainC 运行环境建立完成

关于 __libc_init_array

  • 它会调用所有全局构造函数,C++ 项目必备,__attribute__((constructor)) 修饰的函数也会被调用,目的是main 之前执行初始化
  • 纯 C 项目一般用不到,删掉也没问题;但如果用了 constructor 属性,则必须保留。

至此,C 语言运行环境建立完成main 函数被调用。


五、CubeMX 启动方式的思考

以上就是 CubeMX 生成工程的启动流程——它几乎在汇编启动文件中处理了所有初始化工作

但这里有个值得思考的问题:

拷贝 .data、清零 .bss 这些工作,工具链提供的 CRT 库(C Runtime)其实已经实现了。 CubeMX 的启动文件只是用汇编重写了一遍而已。

那 CRT 是怎么实现的呢?


六、CRT 库的实现:以 picolibc 为例

arm clang 工具链默认使用 picolibc 库,它会提供配套的 crt0 库。crt0 源码主要包含:

  • 一个公共头文件
  • 一个平台相关的源文件

6.1 入口函数 _start

  • M 核架构的入口函数是 _start
  • 通过条件编译预处理适配不同资源(multilib 机制下,不同目录的库就是由这些宏条件编译生成)
  • 最终都会调用 __start(内联函数)

__start 的核心职责:

__start:
  → .data 数据拷贝
  → .bss 清零
  → 调用 main

同样存在一堆条件编译宏,根据不同配置生成 crt 库的多个变种,但核心逻辑一致。

6.2 选择 crt0-minimal

编译脚本中,crt0-minimal 通过将编译选项 CONSTRUCTORS 设为 0,把 __libc_init_array 的调用通过条件编译剔除——对纯 C 项目通常没问题。若要使用 C++,则需选择其它 crt 库变体。


七、改用 CRT 库接管:实操步骤

⚠️ 注意:crt 库源码中用到的那些符号(如 _sidata_sdata_edata_sbss_ebss 等),同样是由链接脚本提供的,链接脚本需要把这些符号与具体运行地址绑定

改造要点(共 2 步)

1. 修改复位中断函数
删掉原本复位中断函数中 .data 拷贝.bss 清零 的操作,直接调用 _start

; 改造前(伪代码)
Reset_Handler:
    bl  SystemInit
    ; ... 拷贝 .data ...
    ; ... 清零 .bss ...
    bl  __libc_init_array
    bl  main

; 改造后(伪代码)
Reset_Handler:
    bl  SystemInit
    bl  _start        ; 交由 CRT 库接管剩余初始化并跳转到 main

2. 确保链接脚本提供 crt 库所需符号
经检查,CubeMX 默认生成的链接脚本已经定义了相关符号,是兼容 crt0 库的,因此通常无需改动

编译与验证

  1. 复制一份汇编启动文件进行修改
  2. app/CMakeLists.txt 中指定新的汇编文件
  3. 在链接选项中指定要链接的运行时库(如 -lcrt0-minimal
  4. 编译工程
  5. 在生成的 map 文件 中确认 crt 库被正确链接 ✅
  6. 烧录程序,复位开发板 —— 运行正常

八、两种方式对比与总结

维度纯汇编启动CRT 库接管
初始化实现汇编手写 .data/.bsscrt0 库标准化实现
可读性/维护性较直观但冗余更标准、简洁
扩展 C++需手动补充构造调用切换 crt 变体即可
换芯片/移植改动较大改动小,接口统一
耦合链接脚本是(不可避免)

结论:

纯汇编启动CRT 库接管 两种方式都可行。但 CRT 库方式更标准化——后续如果加入 C++ 代码更换芯片,改动都会小很多。

同时要注意:使用 CRT 库不可避免会接触到链接脚本。如果对链接脚本不够熟悉,建议先补充相关知识(可参考配套的链接脚本讲解视频)。


附:启动流程总览图(文字版)

[上电 / 复位]
       │
       ▼
[内核复位寄存器 & 清异常]
       │
       ▼
[从地址 0 (aliase → Flash) 读向量表]
       │
       ├── 取栈顶地址 → SP
       └── 取复位中断地址
                │
                ▼
        [Reset_Handler / _start]
                │
                ├── SystemInit()  →  设置 VTOR
                ├── 拷贝 .data    (Flash → RAM)
                ├── 清零 .bss
                ├── __libc_init_array()  (可选, C++/构造函数)
                │
                ▼
             [main()]

本文基于 STM32F103 + arm clang + picolibc 工具链分析,其它 Cortex-M 芯片思路相通。

推荐阅读
关注数
1
文章数
7
业余开发者
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息