4、固件镜像结构:BootROM、Bootloader、主固件分区、签名头部格式设计

好,咱们今天聊聊固件镜像的结构。说白了,就是你的芯片上电后,代码是怎么一步步被加载和执行的。我做了这么多年嵌入式安全,见过太多因为镜像结构设计不合理导致的启动失败,甚至安全漏洞。所以这一节,咱们把每个分区掰开揉碎了讲清楚。

4.1 整体架构概览

一个典型的安全启动固件镜像,通常包含四个核心部分:BootROM、Bootloader、主固件分区,以及签名头部。它们之间的关系,我画了张图,你一看就明白。

核心原则:每一级在加载下一级之前,都必须验证其完整性和合法性。这就是所谓的「信任链」。

安全启动信任链与固件镜像结构 BootROM 硬件固化,不可修改 验证签名 Bootloader 可更新,负责加载主固件 验证签名 主固件分区 应用程序 + 签名头部 签名头部结构 Magic Number 版本号 | 镜像大小 算法ID | 签名值 公钥哈希 | 厂商ID 信任链验证流程 1. BootROM 验证 Bootloader 的签名 → 2. Bootloader 验证主固件的签名 3. 主固件运行时,可进一步验证应用层模块或配置文件的签名 任何一级验证失败,系统立即停止启动并进入安全错误状态

4.2 BootROM:信任的根

BootROM 是芯片出厂时固化在 ROM 里的代码。它不可修改,所以是信任链的起点。我习惯叫它「硬件信任锚点」。

BootROM 的主要职责就两件事:

  • 初始化硬件:时钟、内存控制器、基本外设。让芯片能跑起来。
  • 验证并加载 Bootloader:从指定的存储介质(比如 SPI Flash、eMMC、NAND)中读取 Bootloader,验证其签名,然后跳转执行。

我的经验:BootROM 的代码空间通常非常有限,一般只有 16KB 到 64KB。所以它只能做最核心的事情。我曾经在一个项目里,因为 BootROM 里集成了太多功能,导致 ROM 空间不够,最后不得不砍掉一些非关键特性。记住,BootROM 越精简越安全。

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

Bootloader 是第二级启动代码。它比 BootROM 灵活,可以放在可擦写的 Flash 里,方便更新。但这也意味着它更容易被攻击,所以签名验证必须严格。

Bootloader 通常分两个阶段:

阶段 职责 典型大小
SPL (Secondary Program Loader) 初始化 DDR 内存、时钟、存储控制器 16KB - 64KB
主 Bootloader 加载主固件、验证签名、提供恢复模式 128KB - 512KB

我个人建议,Bootloader 里一定要保留一个「恢复模式」。万一主固件刷坏了,还能通过这个模式救回来。我遇到过客户把主固件刷成砖,就是因为 Bootloader 没有回退机制。

注意:Bootloader 的签名密钥必须与 BootROM 中存储的根公钥匹配。如果密钥泄露,整个信任链就崩塌了。所以,密钥管理是安全启动的核心中的核心。

4.4 主固件分区:应用程序的安身之所

主固件分区存放的就是你的应用程序代码。它通常包含两个区域:

  • 代码区:可执行的二进制代码。
  • 数据区:常量、配置文件、资源文件等。

主固件分区的大小取决于你的 Flash 容量。我见过最小的只有 256KB,大的能到 32MB 甚至更大。设计时要注意对齐——很多 Flash 的擦除块大小是 4KB 或 64KB,分区边界最好对齐到擦除块,否则更新时会很麻烦。

另外,我建议在主固件分区里预留一个「备份区」。OTA 升级时,先下载到备份区,验证通过后再切换。这样即使升级过程中断电,也不会变砖。

4.5 签名头部格式设计

签名头部是固件镜像的「身份证」。它告诉 BootROM 和 Bootloader:这个固件是谁签的、用什么算法、签名值是多少。

下面是我常用的签名头部格式,你可以直接参考:

typedef struct {
    uint32_t magic;          // 魔数,用于识别镜像类型,比如 0x5349474E ("SIGN")
    uint32_t version;        // 固件版本号,用于防回滚
    uint32_t image_size;     // 镜像大小(不含签名头部)
    uint32_t header_size;    // 签名头部大小
    uint32_t algo_id;        // 签名算法标识,比如 0x01=RSA2048, 0x02=ECDSA256
    uint8_t  pubkey_hash[32];// 公钥的 SHA-256 哈希,用于验证公钥完整性
    uint8_t  vendor_id[16];  // 厂商 ID,用于多厂商场景
    uint8_t  reserved[32];   // 保留字段,用于未来扩展
    uint8_t  signature[256]; // 签名值,RSA2048 是 256 字节
} firmware_signature_t;

关键字段说明:

  • magic:防止 BootROM 把随机数据当成合法镜像。我习惯用 ASCII 字符,方便调试时肉眼识别。
  • version:防回滚攻击。Bootloader 会检查新固件的版本号是否大于等于当前版本,否则拒绝升级。
  • pubkey_hash:BootROM 里只存了根公钥的哈希,而不是完整的公钥。这样可以节省 ROM 空间,同时防止公钥被篡改。
  • signature:对整个镜像(从 magic 到 reserved 末尾)的签名。注意,签名值本身不参与签名计算。

4.6 签名验证流程

验证流程其实不复杂,但每一步都不能出错。我拆解一下:

  1. 读取签名头部:从 Flash 中读取固定偏移处的签名头部数据。
  2. 校验魔数:检查 magic 是否匹配。不匹配直接报错。
  3. 校验版本号:检查 version 是否 >= 当前版本。防止回滚。
  4. 计算镜像哈希:对签名头部之后的镜像数据计算哈希值。
  5. 验证签名:用根公钥解密签名值,得到哈希值,与步骤 4 计算的哈希值对比。一致则通过。
  6. 跳转执行:验证通过后,跳转到镜像入口地址执行。

避坑指南:我曾经在一个项目里,因为签名验证时没有检查镜像大小,导致攻击者通过截断镜像的方式绕过了签名验证。从那以后,我每次都会在签名头部里显式记录镜像大小,并在验证时严格检查。

4.7 多镜像与分区表

有些场景下,一个设备里会有多个固件镜像,比如:

  • 主应用程序
  • 无线协议栈(比如 BLE、Wi-Fi)
  • DSP 固件
  • 安全协处理器固件

这时候就需要一个分区表来管理。分区表本身也要签名,防止被篡改。我常用的分区表格式如下:

分区名 起始地址 大小 类型 签名头部偏移
bootloader 0x000000 256KB 可执行 0x000000
app_main 0x040000 2MB 可执行 0x040000
app_backup 0x240000 2MB 可执行 0x240000
config 0x440000 64KB 数据 0x440000
storage 0x450000 剩余空间 文件系统 无

分区表通常放在 Flash 的固定位置,比如偏移 0x000000 处。BootROM 和 Bootloader 都会读取它。

重要:分区表本身必须签名。否则攻击者可以修改分区表,把恶意固件指向合法分区,或者把合法分区指向无效地址。我见过一个案例,就是因为分区表没签名,攻击者通过修改分区表绕过了签名验证。

4.8 小结

固件镜像结构的设计,直接决定了安全启动的可靠性。BootROM 是信任的根,必须固化且不可修改。Bootloader 是承上启下的关键,要支持恢复模式。主固件分区要预留备份区,防止升级失败变砖。签名头部是固件的身份证,每个字段都有其存在的意义。

嗯,这一节的内容就到这里。记住,设计镜像结构时,多想想「如果攻击者篡改了这个字段,会发生什么?」。带着这个问题去设计,你的系统会更安全。