4. BootROM 设计:一级 BootROM 启动流程、公钥存储与校验、安全启动使能
好,咱们今天聊点硬核的——BootROM。很多做应用层的朋友可能觉得这东西离自己很远,其实不然。你写的每一行固件,最终能不能安全地跑起来,全靠这巴掌大的 ROM 代码在背后撑着。
我个人习惯把 BootROM 比作芯片的「接生婆」。上电那一刻,CPU 还懵着呢,是 BootROM 告诉它:兄弟,该干活了,先去这儿把公钥读出来,再去那儿验个签名。嗯,今天我们就把它拆开看看。
4.1 一级 BootROM 启动流程
芯片上电后,PC 指针会跳到一个固定的地址——通常是 0x00000000 或者 0xFFFF0000,这取决于架构。这个地址里放的就是 BootROM 的第一条指令。
一级 BootROM 的流程,说白了就三步:初始化、加载、跳转。但每一步里都有坑。
- 硬件自检(HWR):检查 CPU、SRAM、时钟、PLL 是否正常。我遇到过某款芯片,PLL 锁定时间不够,导致 BootROM 读到的是乱码。后来加了 500us 的延时才搞定。
- 安全引擎初始化:如果芯片有硬件加密引擎(如 AES、RSA、SHA),这一步要把它唤醒。注意,有些芯片的安全引擎默认是关闭的,需要写特定寄存器才能打开。
- 启动介质检测:BootROM 会按照预设的优先级去检测启动介质。比如先看 SPI NOR Flash,再看 NAND,最后看 SD 卡。检测方式通常是读介质的头部几个字节,判断是否为合法的启动头。
- 加载一级 Bootloader(SPL/IBL):从检测到的介质中,把一级 Bootloader 加载到内部 SRAM 中。这里有个关键点——加载地址。BootROM 会把数据放到哪个地址?这取决于芯片设计。我建议你查一下芯片手册里的 "BootROM Load Address" 字段。
- 验签(如果使能):如果安全启动使能了,BootROM 会在跳转前对加载的数据进行签名验证。验证通过才跳转,否则死循环或复位。
- 跳转:修改 PC 指针,指向 SRAM 中一级 Bootloader 的入口地址。
你可能会问:为什么不用外部 DDR?因为 BootROM 阶段,DDR 还没初始化呢!所以一级 Bootloader 必须放在内部 SRAM 里,大小通常只有几十 KB 到几百 KB。这也是为什么一级 Bootloader 要写得非常精简。
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)。
公钥校验的代码,大致长这样:
// 伪代码:公钥校验流程
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 来使能安全启动。
使能安全启动,一般分三步:
- 烧写公钥:把开发阶段生成的公钥烧写到 OTP/eFuse 中。
- 烧写使能位:把安全启动使能位(通常是一个 eFuse 位)烧写为 1。这一步是不可逆的!
- 验证:复位芯片,观察是否能正常启动。如果启动失败,说明签名或公钥有问题,但此时 eFuse 已经烧了,芯片就变砖了。
安全启动使能后,BootROM 的流程会多出一个分支:
// 伪代码:安全启动使能判断
if (is_secure_boot_enabled()) {
// 使能了,必须验签
if (verify_signature() != 0) {
while(1); // 死循环,拒绝启动
}
} else {
// 没使能,直接跳转
// 注意:这里存在安全风险
jump_to_loader();
}
你可能会问:如果安全启动没使能,是不是就不验签了?是的。但这也意味着,攻击者可以替换 Flash 里的固件,直接启动。所以,量产产品一定要使能安全启动。
4.4 整体流程可视化
说了这么多,咱们用一张图把整个流程串起来。下面这张 SVG 图展示了一级 BootROM 从上电到跳转的完整路径,以及安全启动使能后的分支。
从这张图可以看得很清楚:安全启动使能后,BootROM 会在跳转前强制验签。验签失败就死循环,不给攻击者任何机会。非安全模式下,虽然流程简单,但风险也大。
4.5 小结
一级 BootROM 是整个安全启动的基石。它虽然代码量不大,但每一行都关乎生死。公钥存储的位置、校验的方式、安全启动使能的时机,任何一个环节出问题,都可能导致整个安全体系崩塌。
嗯,我记得有一次帮客户调试,发现他们的 BootROM 在验签时居然没有检查公钥的长度,导致攻击者传入一个超长的公钥,触发了缓冲区溢出。这种问题,说白了就是设计时没考虑边界情况。咱们做嵌入式安全的,一定要有「攻击者思维」——假设所有输入都是恶意的,然后去防御。