裸机固件没有桌面操作系统先替程序建立进程。以普通 Cortex-M 启动为例,处理器从启动映射的向量表读取初始主栈指针和复位处理函数地址,随后执行启动代码。向量表在哪里、Flash 怎样映射、从哪里启动,仍取决于芯片与启动配置。

main 之前发生什么

启动代码、系统初始化和 C 运行库共同建立程序要求的环境。常见工作包括初始化时钟的基础状态,把带初值的可写数据从 Flash 装载地址复制到 RAM,清零未显式初始化的静态存储区,初始化 C/C++ 运行时,再调用 main。具体顺序要读工具链配套启动文件,不能假定所有厂商都完全一样。

区域常见内容固件文件与运行时的关系
.text / .rodata机器码、只读数据通常位于 Flash,部分场景复制到 RAM 执行
.data有非零初值的全局/静态可写变量初始值存于映像,运行时位于 RAM
.bss需置零的全局/静态变量通常在映像中记录范围,启动时清零
栈调用帧、局部变量、异常现场运行时占用 RAM,需保留最坏情况余量
堆动态分配区是否存在及增长方式由运行库和链接配置决定

链接脚本负责把段安排到具体地址,并导出启动代码要用的边界符号。装载地址 LMA 与运行地址 VMA 可以不同。修改 RAM 大小后只改一个宏而没改链接脚本,可能出现链接成功却覆盖保留区的问题;map 文件能帮助核对每段大小和位置。

寄存器访问与 volatile

内存映射 I/O 把特定地址的读写解释为外设操作。访问前要核对外设时钟、寄存器宽度、位语义和允许的访问顺序。

/* 表达访问方式;地址必须来自实际芯片头文件或参考手册。 */
static inline void write_register(volatile uint32_t *reg, uint32_t value) {
    *reg = value;
}

volatile 要求编译器按语言和实现约定保留相关访问,不提供线程同步,也不保证复合操作原子。reg |= mask 是读—改—写;遇到写 1 清零、只写寄存器或并发修改时,可能产生额外副作用。优先使用芯片专门的置位/复位寄存器或按手册规定写入。

在主机观察启动模型

下载 启动内存模型,运行 python startup.py。脚本把 RAM 先填入非零模式,再执行 .data 复制与 .bss 清零,检查边界外内存没有被破坏。它还解析一张小端向量表,核对栈地址和 Thumb 入口位。

模型验证数据搬运逻辑,不模拟 Cortex-M 指令。实物调试时可在 Reset_Handler 和 main 分别设断点,观察 .data/.bss、SP、PC 与 map 文件。优化可能合并或删除变量,观察目标应选确实保留在映像中的对象。

排查启动故障

若进不了 main,依次确认供电/复位和启动模式、向量表、栈范围、镜像烧录地址、系统时钟设置,再查初始化过程中的异常。HardFault 分析应保存异常现场、返回地址及相关状态寄存器,并对应反汇编;只看到“进了 HardFault”不足以定位越界、非法指令或总线访问错误。

练习:故意把模型的 .bss 结束地址加大,确认边界测试失败;在一份真实 map 文件中找到最大的三个 RAM 对象,解释为何固件文件大小不能代表 RAM 占用。

面试自查:未初始化的局部变量为什么不保证为零?.data 为什么同时占 Flash 和 RAM?volatile 能否保护 ISR 与主循环之间的计数器?Bootloader 跳转应用时除了改 PC 还要考虑什么?

参考:Arm CMSIS 启动文件、GNU ld:LMA 与 VMA。