3. 固件镜像结构:BootROM、Bootloader、主固件分区、元数据与签名区域布局

这一章咱们聊聊固件镜像到底长什么样。说白了,就是搞清楚芯片上电后,代码和数据是怎么在 Flash 里排排坐的。

我刚开始做嵌入式安全的时候,总觉得只要把代码写对就行,布局无所谓。直到有一次,BootROM 因为分区表偏移算错了一个字节,直接跳到了空白区域——板子变砖了。从那以后,我对镜像布局就再也不敢马虎了。

3.1 整体布局概览

一个典型的安全启动固件镜像,从 Flash 起始地址开始,通常包含以下几个区域:

  • BootROM:芯片出厂固化的只读代码,不可修改。
  • Bootloader:第一阶段启动代码,负责校验和加载主固件。
  • 主固件分区:真正的应用程序或操作系统镜像。
  • 元数据区域:描述固件版本、大小、哈希值等信息的结构化数据。
  • 签名区域:存放数字签名,用于验证固件的完整性和来源。

你想想看,如果这些区域随便乱放,BootROM 怎么知道该跳到哪里去执行?所以,布局是有严格约定的。

核心原则:每个区域的起始地址和大小,必须在芯片设计阶段就固定下来,或者在 BootROM 中通过硬编码的偏移量访问。我见过不少项目因为后期调整分区大小,导致 BootROM 里的地址写死,不得不重新流片——那成本可就大了去了。

固件镜像布局结构图 固件镜像 Flash 布局 Flash 地址空间(从 0x00000000 开始) BootROM(只读,芯片出厂固化) Bootloader(第一阶段启动代码) 主固件分区(应用程序 / OS 镜像) 元数据(版本/哈希/大小) 签名区域(数字签名) 说明:实际布局中元数据和签名区域可能位于主固件之前或之后,具体取决于芯片架构

3.2 BootROM:不可篡改的信任根

BootROM 是芯片上电后执行的第一段代码。它被固化在芯片内部的只读存储器里,出厂后谁都没法改——包括你自己。

我记得有一次跟芯片原厂沟通,想让他们帮忙修一个 BootROM 的小 bug。对方回复说:修可以,但得等下一版芯片,周期至少半年。嗯,从那以后我就明白了,BootROM 的代码必须零缺陷。

BootROM 的核心职责就两件事:

  1. 初始化最基本的硬件(时钟、SRAM、Flash 控制器)。
  2. 从固定地址加载 Bootloader,并跳转过去执行。

注意:BootROM 通常只有几十 KB,甚至几 KB。它没有复杂的功能,连中断都不开。我见过有人试图在 BootROM 里塞 USB 协议栈——结果空间不够,连最基本的 Flash 驱动都放不下了。

3.3 Bootloader:承上启下的关键环节

Bootloader 是第一个可被厂商更新的代码。它由 BootROM 加载,负责更复杂的初始化工作,以及最重要的——校验主固件的完整性。

我个人习惯把 Bootloader 分成两个阶段:

  • 第一阶段(SPL / 最小启动):初始化 DDR、时钟,加载第二阶段。
  • 第二阶段(完整 Bootloader):文件系统支持、网络启动、固件校验、回滚逻辑。

为什么这么分?因为 BootROM 通常只能访问内部 SRAM,空间有限。先把一个极简的 SPL 加载进来,它再去初始化 DDR,然后把完整的 Bootloader 拷贝到 DDR 里运行。说白了,就是「小马拉大车」——一步一步来。

经验之谈:我曾经在一个项目里把 Bootloader 做得太大,结果 BootROM 加载时超时了。后来我强制要求 Bootloader 的第一阶段不超过 32KB,第二阶段不超过 256KB。这个经验后来成了我们团队的分区规范。

3.4 主固件分区:真正的战场

主固件分区存放的就是你的应用程序或者操作系统镜像。它通常是整个 Flash 里最大的区域,可能占几 MB 甚至几百 MB。

这里有个关键点:主固件分区必须包含一个固件头。这个头里记录了:

字段 大小 说明
魔数 4 字节 用于快速识别是否为有效固件(如 0xDEADBEEF)
版本号 4 字节 主版本.次版本.修订号
固件大小 4 字节 不包括头部本身的固件长度
哈希值 32 字节 SHA-256 摘要,用于完整性校验
加载地址 4 字节 固件应该被加载到内存的哪个地址
入口点 4 字节 执行第一条指令的地址

你想想看,如果没有这个头,Bootloader 怎么知道该把固件加载到哪里?怎么知道它是不是被篡改过?所以,固件头不是可选项,是必需品。

3.5 元数据区域:固件的「身份证」

元数据区域存放的是描述固件本身的信息。它和固件头有点像,但范围更广——它描述的是整个镜像,而不仅仅是主固件。

元数据通常包含:

  • 镜像版本列表(支持多版本共存时)
  • 每个分区的偏移量和大小
  • 分区的哈希值(用于快速校验)
  • 回滚计数器(防止降级攻击)
  • 安全配置(如是否强制校验签名)

我曾经遇到过一个坑:元数据区域没有做备份。有一次 Flash 在写入元数据时突然掉电,结果整个分区表损坏,Bootloader 找不到任何固件。从那以后,我坚持元数据必须保留两份冗余副本。

建议:元数据区域放在主固件之前,这样 Bootloader 可以先读取元数据,再根据元数据里的偏移量去加载主固件。如果放在后面,Bootloader 就得先扫描整个 Flash 才能找到它——效率太低了。

3.6 签名区域:安全启动的最后一道防线

签名区域存放的是对整个固件镜像(或至少对主固件 + 元数据)的数字签名。这个签名由厂商的私钥生成,BootROM 或 Bootloader 用对应的公钥来验证。

签名区域的结构通常很简单:

// 签名区域布局示例(RSA-2048 + SHA-256)
// 总大小:256 字节(RSA 签名)+ 4 字节(签名算法标识)
typedef struct {
    uint32_t signature_algorithm;   // 0x01 = RSA-2048/SHA-256
    uint8_t  signature[256];        // RSA 签名值
    uint32_t reserved;              // 对齐填充
} signature_region_t;

这里有个容易被忽略的细节:签名区域本身必须是只读的。如果攻击者能篡改签名区域,那整个安全启动就形同虚设了。我见过一个产品,签名区域放在可写的文件系统里——结果被黑客直接替换了签名,用自己的固件启动了。

警告:签名区域必须放在 Flash 的物理写保护区域,或者通过 eFuse 锁定。不要依赖软件层面的只读标志——那玩意儿在攻击者面前跟纸糊的一样。

3.7 布局示例:一个实际的 Flash 分区表

下面是我在一个 IoT 项目里用过的分区表,芯片是某款 ARM Cortex-M4,Flash 总大小 2MB:

区域 起始地址 大小 说明
BootROM 0x0000_0000 64 KB 芯片内部 ROM,不可修改
Bootloader 0x0001_0000 128 KB 第一阶段 + 第二阶段
元数据(主副本) 0x0003_0000 4 KB 分区表 + 版本信息
元数据(备份副本) 0x0003_1000 4 KB 冗余备份
主固件分区 A 0x0003_2000 896 KB 当前运行版本
主固件分区 B 0x0010_2000 896 KB 升级备用版本(A/B 切换)
签名区域 0x001D_2000 4 KB 存放两个分区的签名
用户数据 0x001D_3000 剩余空间 配置文件、日志等

你注意到没有?我把签名区域放在了最后,而且只给了 4KB。为什么?因为签名本身只需要 256 字节,4KB 已经留了足够的余量。更重要的是,我把签名区域放在 Flash 的末尾,方便用硬件写保护一次性锁定整个区域。

嗯,这就是我这些年总结出来的布局经验。每个芯片的 BootROM 要求可能不一样,但核心思路是相通的:信任链要短,关键数据要冗余,签名区域要锁定。记住这三点,你的固件镜像结构就不会出大问题。