4. BootROM 设计:一级 BootROM 启动流程、公钥存储与校验、安全启动使能

好,咱们今天聊点硬核的——BootROM。很多做应用层的朋友可能觉得这东西离自己很远,其实不然。你写的每一行固件,最终能不能安全地跑起来,全靠这巴掌大的 ROM 代码在背后撑着。

我个人习惯把 BootROM 比作芯片的「接生婆」。上电那一刻,CPU 还懵着呢,是 BootROM 告诉它:兄弟,该干活了,先去这儿把公钥读出来,再去那儿验个签名。嗯,今天我们就把它拆开看看。

4.1 一级 BootROM 启动流程

芯片上电后,PC 指针会跳到一个固定的地址——通常是 0x00000000 或者 0xFFFF0000,这取决于架构。这个地址里放的就是 BootROM 的第一条指令。

一级 BootROM 的流程,说白了就三步:初始化、加载、跳转。但每一步里都有坑。

核心流程:
  1. 硬件自检(HWR):检查 CPU、SRAM、时钟、PLL 是否正常。我遇到过某款芯片,PLL 锁定时间不够,导致 BootROM 读到的是乱码。后来加了 500us 的延时才搞定。
  2. 安全引擎初始化:如果芯片有硬件加密引擎(如 AES、RSA、SHA),这一步要把它唤醒。注意,有些芯片的安全引擎默认是关闭的,需要写特定寄存器才能打开。
  3. 启动介质检测:BootROM 会按照预设的优先级去检测启动介质。比如先看 SPI NOR Flash,再看 NAND,最后看 SD 卡。检测方式通常是读介质的头部几个字节,判断是否为合法的启动头。
  4. 加载一级 Bootloader(SPL/IBL):从检测到的介质中,把一级 Bootloader 加载到内部 SRAM 中。这里有个关键点——加载地址。BootROM 会把数据放到哪个地址?这取决于芯片设计。我建议你查一下芯片手册里的 "BootROM Load Address" 字段。
  5. 验签(如果使能):如果安全启动使能了,BootROM 会在跳转前对加载的数据进行签名验证。验证通过才跳转,否则死循环或复位。
  6. 跳转:修改 PC 指针,指向 SRAM 中一级 Bootloader 的入口地址。

你可能会问:为什么不用外部 DDR?因为 BootROM 阶段,DDR 还没初始化呢!所以一级 Bootloader 必须放在内部 SRAM 里,大小通常只有几十 KB 到几百 KB。这也是为什么一级 Bootloader 要写得非常精简。

我的经验: 调试 BootROM 流程时,最痛苦的是没有打印。芯片刚上电,串口都没初始化,你怎么看日志?我常用的办法是:GPIO 点灯。在关键步骤前后翻转 GPIO,用逻辑分析仪抓波形。虽然土,但有效。

4.2 公钥存储与校验

公钥放在哪?这是个好问题。你想想看,如果公钥也放在 Flash 里,攻击者把 Flash 拆下来,把公钥换成自己的,那签名验证不就形同虚设了吗?

所以,公钥必须存储在不可篡改的区域。常见的方案有这几种:

存储位置 优点 缺点 常见场景
OTP(一次性可编程存储器) 物理不可篡改,出厂后锁定 容量小(通常几 KB),写入后不可改 主流方案,ARM Cortex-M 系列常用
eFuse 可逐位烧写,灵活性高 需要高压编程,工艺复杂 高端 SoC,如手机芯片
ROM Mask 最安全,掩膜时直接固化 无法更新,一旦有 bug 只能流片 极其成熟的芯片,如某些 MCU
安全 Flash 区域 可更新,容量大 依赖 Flash 保护机制,可能被绕过 低安全要求的场景

我个人最推荐的是 OTP + eFuse 组合。OTP 里存公钥的哈希值,eFuse 里存公钥本身。验证时,先读 eFuse 里的公钥,算哈希,再跟 OTP 里的哈希比对。这样既保证了公钥的可更新性(通过 eFuse),又保证了不可篡改性(通过 OTP)。

注意: 公钥校验时,一定要做完整校验。我曾经见过一个案例,某厂商只校验了公钥的前 16 个字节,结果攻击者构造了一个前 16 字节相同、后面不同的假公钥,成功绕过了验证。嗯,这种低级错误,咱们不能犯。

公钥校验的代码,大致长这样:

// 伪代码:公钥校验流程
int verify_public_key(uint8_t *pubkey, uint32_t len) {
    uint8_t hash[32];
    uint8_t otp_hash[32];
    
    // 1. 从 OTP 中读取预存的公钥哈希
    read_otp(OTP_PUBKEY_HASH_ADDR, otp_hash, 32);
    
    // 2. 计算当前公钥的 SHA-256 哈希
    sha256(pubkey, len, hash);
    
    // 3. 比较
    if (memcmp(hash, otp_hash, 32) != 0) {
        // 校验失败,进入错误处理
        secure_boot_fail();
        return -1;
    }
    
    return 0;
}

4.3 安全启动使能

安全启动不是默认打开的。芯片出厂时,通常处于非安全模式,方便开发调试。等到产品量产时,再通过烧写 eFuse 或 OTP 来使能安全启动。

使能安全启动,一般分三步:

  1. 烧写公钥:把开发阶段生成的公钥烧写到 OTP/eFuse 中。
  2. 烧写使能位:把安全启动使能位(通常是一个 eFuse 位)烧写为 1。这一步是不可逆的!
  3. 验证:复位芯片,观察是否能正常启动。如果启动失败,说明签名或公钥有问题,但此时 eFuse 已经烧了,芯片就变砖了。
避坑指南: 我曾经在量产时犯过一个错误——先烧了使能位,再烧公钥。结果使能位烧写后,芯片立即进入安全启动模式,但此时公钥还没烧进去,导致所有芯片都变砖了。那次损失不小。所以,顺序很重要:先烧公钥,再烧使能位。

安全启动使能后,BootROM 的流程会多出一个分支:

// 伪代码:安全启动使能判断
if (is_secure_boot_enabled()) {
    // 使能了,必须验签
    if (verify_signature() != 0) {
        while(1); // 死循环,拒绝启动
    }
} else {
    // 没使能,直接跳转
    // 注意:这里存在安全风险
    jump_to_loader();
}

你可能会问:如果安全启动没使能,是不是就不验签了?是的。但这也意味着,攻击者可以替换 Flash 里的固件,直接启动。所以,量产产品一定要使能安全启动。

4.4 整体流程可视化

说了这么多,咱们用一张图把整个流程串起来。下面这张 SVG 图展示了一级 BootROM 从上电到跳转的完整路径,以及安全启动使能后的分支。

一级 BootROM 安全启动流程图 一级 BootROM 安全启动流程图 上电复位 硬件自检 (HWR) 安全引擎初始化 启动介质检测 加载一级 Bootloader 安全启动 使能? 是 验签 验签 通过? 否 死循环 是 跳转 否 直接跳转 注意:非安全模式下 不验签,存在安全风险

从这张图可以看得很清楚:安全启动使能后,BootROM 会在跳转前强制验签。验签失败就死循环,不给攻击者任何机会。非安全模式下,虽然流程简单,但风险也大。

4.5 小结

一级 BootROM 是整个安全启动的基石。它虽然代码量不大,但每一行都关乎生死。公钥存储的位置、校验的方式、安全启动使能的时机,任何一个环节出问题,都可能导致整个安全体系崩塌。

嗯,我记得有一次帮客户调试,发现他们的 BootROM 在验签时居然没有检查公钥的长度,导致攻击者传入一个超长的公钥,触发了缓冲区溢出。这种问题,说白了就是设计时没考虑边界情况。咱们做嵌入式安全的,一定要有「攻击者思维」——假设所有输入都是恶意的,然后去防御。