4、BL2 (Trusted Boot Firmware):可信引导固件,验证后续镜像

好,咱们接着聊。上一节我们把 BL1 讲透了,它就像个「开锁匠」,把安全世界的大门打开了一条缝。现在门开了,谁进来?BL2 登场了。

BL2 的全称是 Trusted Boot Firmware,中文叫「可信引导固件」。我个人习惯把它理解为整个启动流程的「守门员」——它不负责具体干活,但它要确保接下来进场的每一个角色都是「自己人」。

4.1 BL2 到底在干什么?

说白了,BL2 的核心任务就两件:验证 + 加载。

  • 验证:检查后续镜像(BL31、BL32、BL33)的数字签名和哈希值。
  • 加载:把验证通过的镜像从非易失存储(比如 SPI Flash、eMMC)拷贝到 DRAM 里。

你想想看,如果 BL1 把 BL2 拉起来了,但 BL2 是个「内鬼」,那整个系统就完蛋了。所以 BL2 本身必须是可信的——它的签名在 BL1 阶段就已经被验证过了。

关键点:BL2 是信任链的第二个环节。信任链一旦断裂,后续所有安全保证都是空谈。

4.2 BL2 的典型执行流程

我在项目中调试过好几款 SoC 的 BL2,流程大同小异。这里画个图,大家一看就明白。

BL2 可信引导流程 1. 初始化平台 时钟、UART、MMU 2. 读取 FIP Firmware Image Package 3. 解析镜像 BL31/32/33 4. 验证镜像签名 & 哈希 使用公钥验证数字签名 验证失败 验证通过 5. 加载镜像到 DRAM 设置入口地址,跳转到 BL31 进入 BL31 (EL3)

嗯,这里要注意:验证失败的处理。我在早期的一个项目里,BL2 验证失败后直接 panic 了,连个错误码都没打印。结果调试的时候一脸懵,不知道是镜像坏了还是签名错了。后来我加了个详细的错误报告——验证失败时打印出「哪个镜像、哪个字段、期望值 vs 实际值」。这个习惯我一直保留到现在。

4.3 FIP:固件镜像包

BL2 读取的 FIP 是个什么东西?说白了,它就是一个「打包文件」。把 BL31、BL32、BL33 以及它们的证书、签名全部塞到一个文件里。BL2 解析 FIP,就像你解压一个 ZIP 包一样。

FIP 的结构大致如下:

偏移 内容 说明
0x0000 TOC Header 表头,包含镜像数量、校验和
0x0040 Entry 1: BL31 描述 BL31 的偏移、大小、UUID
0x0080 Entry 2: BL32 描述 BL32 的信息
0x00C0 Entry 3: BL33 描述 BL33 的信息
0x0100 BL31 镜像数据 实际的二进制内容
... ... ...

小技巧:调试时可以用 fiptool 工具查看 FIP 内容。命令:fiptool info --verbose fip.bin。我曾经靠这个工具发现过镜像 UUID 写错的问题,省了两天调试时间。

4.4 验证机制:信任链的基石

BL2 的验证机制,我把它拆成三步:

  1. 读取证书:FIP 里每个镜像都附带一个 X.509 证书。
  2. 验证证书链:从根证书到镜像证书,逐级验证。根证书的公钥是固化在 BL1 或 BL2 代码里的。
  3. 验证镜像哈希:用证书里的公钥解密签名,对比镜像的哈希值。

为什么会这样设计?你想想看,如果只验证签名,那签名本身可能被伪造。但有了证书链,攻击者必须拿到根证书的私钥才能伪造——这个私钥通常存储在 HSM(硬件安全模块)里,物理上都拿不到。

避坑指南:我曾经遇到过一个 case,BL2 验证总是失败。查了两天发现是根证书的公钥在 BL1 里存错了——一个字节的偏移搞反了。所以,公钥的存储位置和格式一定要和签名工具生成的一致。建议在 BL1 启动时打印公钥的哈希,和签名工具输出的哈希比对一下。

4.5 BL2 的代码结构(以 ARM Trusted Firmware 为例)

ATF 里 BL2 的代码主要在 bl2/ 目录下。核心文件就几个:

  • bl2_main.c:主入口,调用各个初始化函数。
  • bl2_image_load.c:镜像加载逻辑。
  • auth/:验证模块,处理证书和签名。

我摘一段核心代码,大家感受一下:

// bl2_main.c 简化版
void bl2_main(void)
{
    /* 1. 初始化控制台 */
    console_init();

    /* 2. 初始化存储设备 */
    io_setup();

    /* 3. 加载 FIP 到内存 */
    load_fip();

    /* 4. 验证并加载 BL31 */
    if (load_and_auth_image(BL31_IMAGE_ID) != 0) {
        ERROR("BL31 authentication failed!\n");
        panic();
    }

    /* 5. 验证并加载 BL32(如果有) */
    if (plat_get_image_desc(BL32_IMAGE_ID)) {
        load_and_auth_image(BL32_IMAGE_ID);
    }

    /* 6. 验证并加载 BL33 */
    load_and_auth_image(BL33_IMAGE_ID);

    /* 7. 跳转到 BL31 */
    bl31_entrypoint();
}

这段代码看着简单,但实际项目里每个函数背后都有几百行的细节。比如 load_and_auth_image() 里要处理不同的存储介质(NOR Flash、NAND、eMMC),还要处理不同的加密算法(RSA、ECDSA)。

4.6 实际项目中的 BL2 调试经验

我记得有一次,客户反馈说他们的板子启动到 BL2 就卡死了。我远程一看,BL2 打印了 "Loading BL31..." 之后就没了下文。典型的「加载卡死」问题。

排查步骤:

  1. 先确认 FIP 是否烧录正确——用 fiptool 检查,没问题。
  2. 再确认存储驱动——用示波器量 SPI Flash 的时钟和数据线,发现时钟频率太高,Flash 响应不过来。
  3. 降低 SPI 时钟频率后,问题解决。

你看,有时候问题不在 BL2 的逻辑本身,而在底层硬件。所以我的习惯是:调试 BL2 时,先确保硬件通信正常。打印几行 "Read from offset X, got data Y" 之类的调试信息,能省很多事。

4.7 BL2 的优化方向

BL2 的启动时间直接影响整个系统的启动速度。我见过一些优化手法:

  • 并行验证:如果 SoC 有多个加密引擎,可以同时验证 BL31 和 BL32。
  • 延迟加载:某些镜像(比如 BL32)可以等 BL31 启动后再加载,减少 BL2 阶段的工作量。
  • 缓存 FIP:如果 FIP 存储在慢速设备上,可以先把整个 FIP 读到 SRAM 里再解析。

不过说实话,这些优化要谨慎。我在一个项目里尝试并行验证,结果两个加密引擎抢总线,反而更慢了。嗯,优化这种事,还是要实测数据说话。

好了,BL2 就聊这么多。它是信任链的第二个环节,承上启下。验证通过后,它会把控制权交给 BL31——也就是 EL3 的运行时固件。那才是真正精彩的地方。

一句话总结:BL2 是「守门员」,它用证书链和哈希验证确保后续镜像可信,然后加载到内存,为进入 EL3 做好准备。

可可精品资料库