☰
ATF安全启动实战:BL2认证框架与信任链交棒全解析
2026/10/3 11:32:40 网站建设 项目流程

这篇是这个系列的第二篇,标题里那个(2)意味着我默认你已经对 ATF 的镜像分类和冷启动路线有了基本概念。这篇我们把 SOC-ATF 的安全启动链路收窄到 BL2 这一层,从 BL1 交棒那几条汇编指令开始,一路跟到 BL2 把 BL31 拉起来。如果你正在做固件安全适配、TBB 移植,或者只是想知道 BL2 的认证框架到底怎么工作,这篇文章可以当作一张比较完整的地图用。

先说明一下范围:本文分析以 TF-A 2.6 到 2.8 的经典代码路径为主,结尾会补一段新版 Transfer List 的变化。BL2 在安全启动里的角色一句话能概括:BL1 信任它,它验证 BL31/BL32/BL33,然后交棒。这句话背后的执行细节,值得用一整篇来拆。

1. BL2 在信任链里的定位:BL1 交棒后它拿到了什么

很多人看 BL2 代码喜欢直接打开 bl2_main.c 往下啃,这样容易卡住,因为 BL2 的起点其实不在 C 代码里。BL2 是被 BL1 用一条跳转指令送进来的,而在那条指令之前,BL1 已经把一堆关键数据结构通过寄存器传了下来。不搞懂这层交接,后面所有代码都会看得一头雾水。

1.1 BL1 交棒时刻的寄存器与数据结构

在经典 TF-A 流程里,BL1 完成对 BL2 镜像的认证之后,会走到bl1_run_bl2(),这里会填充一个bl2_ep_info(entry point info),然后把几个参数塞进bl2_ep_info->args.arg0到arg3,最终通过bl2_entrypoint(x0, x1, x2, x3)跳转。

这几个参数里最重要的是一组指向 bl2_params 结构体的指针。bl2_params_t的定义不长,但信息密度很高,它把 BL2 接下来要填充的下一级镜像信息全部包括了:

  • bl31_ep_info:指向entry_point_info_t,记录 BL31 的入口地址、执行状态、SPSR 设置等。
  • bl32_image_info:指向 Trusted OS(例如 OP-TEE)的镜像信息。
  • bl33_image_info:指向非安全世界的引导程序,比如 U-Boot 或 UEFI。

除此之外还有一组bl2_mem_params_descs内存描述符表,描述每个镜像在 FIP 里的位置、加载地址、大小以及内存属性。BL1 把这些地址通过寄存器传下来,BL2 的入口汇编不会去改动它们,等到 C 代码里再作为参数解析。

为什么这套接口这么重要?因为 BL1 通常固死在 BootROM 里,没法升级。而 BL2 是可以升级的。如果 BL1 和 BL2 之间的参数约定不够稳定,那芯片出厂之后 BL2 一升级就可能导致整个启动链断掉。所以 TF-A 把交棒接口设计得极其克制,能用结构体指针解决的问题绝不额外定义复杂协议。

实际调试时如果你用 JTAG 直接拉 PC 到 BL2 入口,会发现这几个寄存器里是空的,BL2 后面读参数时就会拿到垃圾值。很多"BL2 进来就崩"的问题,根因其实是调试者跳过了 BL1 交棒过程,而不是 BL2 本身的 bug。

1.2 入口汇编给 C 代码铺了什么路

BL2 的入口在bl2/aarch64/bl2_entrypoint.S,函数名就叫bl2_entrypoint。这段汇编做的事情比一般人想象中要多,它不仅仅是把栈指针指一下然后调 C 函数。

整个入口的逻辑大致是这样的:

  1. 保存 BL1 传入的参数寄存器,确保后面 C 代码能用。
  2. 设置异常向量表地址。
  3. 设置 BL2 自己的栈指针。
  4. 如果平台配置了,可能还要做一次 cache 清理和无效化,避免 BL1 阶段留下的脏数据影响后面执行。
  5. 调用bl2_early_platform_setup(),这一步里面会初始化串口、存储控制器等基础外设。
  6. 调用bl2_plat_arch_setup(),这一步建立 MMU 页表并开启 MMU。
  7. 跳转到bl2_main(),正式进入 C 代码主流程。

为什么 BL2 不在一进来就开 MMU?因为早平台初始化阶段可能需要操作物理地址下的寄存器,开着 MMU 反而要处理地址映射的边界问题。TF-A 的做法是先做最基础的硬件初始化,再用bl2_plat_arch_setup把整个 BL2 运行所需的地址空间一次性映射好。这个顺序几乎在所有 ARM 平台是一致的,新平台移植的时候不要自己瞎改顺序。

还有一点值得注意:BL2 的 BSS 段通常在早期链接脚本里就规划好了,入口汇编在调 C 函数之前会用它自己生成的 BSS 符号把这块区域清零。如果平台链接脚本里__BSS_START__和__BSS_END__定义错了,效果就是全局变量初始值全乱,症状会很诡异。

1.3 单核执行与 BL2_AT_EL3 的选择

BL2 阶段默认只在一个核上跑,这由COLD_BOOT_SINGLE_CPU这类平台配置决定。原因很好理解:BL2 要做的事情是认证和加载镜像,多核同时跑不仅没有收益,还会引入核间竞争和一致性问题。毕竟这个时候 MMU 都可能刚开,多核同步的复杂度远大于收益。

另一个容易混淆的宏是BL2_AT_EL3。在标准 ARM 平台上,BL1 负责把 BL2 引导到 EL3 并搭好基本执行环境,BL2 只需要在这个环境里继续干活。但有些平台没有 BL1 的概念,比如树莓派或者 QEMU,它们直接从自己的 bootloader 跳到 BL2,此时 BL2 需要自己承担更多初始化工作。BL2_AT_EL3就是告诉 BL2:你现在直接跑在 EL3 上,没有人帮你把 MMU、异常向量、中断控制器这些准备好,你得自己来。

这个宏的效果在链接脚本和入口汇编里都有体现。开启后,BL2 会被链接到独立的地址空间,入口汇编也会多做一些自举动作。刚开始做平台移植的同学如果发现 BL2 一启动就异常,先检查这个宏的配置是否符合平台的实际启动场景。

2. BL2 的平台初始化与内存布局:安全边界不是一句空话

安全启动经常被简化成"验签"两个字,但实际工程里,BL2 的代码放在哪里、内存怎么布局,本身就是安全边界的一部分。如果 BL2 运行在非安全内存里,那后面做的任何认证都没有意义,因为攻击者可以 DMA 改写它的代码。

2.1 跑 BL2 的 TZRAM 是谁划出来的

在 ARM FVP 这类标准平台上,BL2 运行在 TZRAM(TrustZone RAM)里,BL31 则运行在 TZDRAM。这些区域的地址和大小由平台头文件里的宏定义,比如TZRAM_BASE、TZRAM_SIZE,最终体现在 BL2 的链接脚本bl2.ld.S里。

链接脚本会生成一组符号,比如__BL2_START__、__BL2_END__、__BSS_START__、__BSS_END__,这些符号不仅决定 BL2 镜像的布局,还会被 BL1 用来确认 BL2 镜像的加载范围。如果你修改了 BL2 镜像大小但没有同步调整 TZRAM_SIZE,BL1 在拷贝 BL2 时可能把数据写到后面的保留区域,运行时会踩到 BL31 的领地。

这里有个经常被忽略的点:BL2 镜像的加载地址(LMA)和运行地址(VMA)可以不一样。BL1 先把 BL2 从存储介质读到一个临时位置,再根据平台配置搬运到最终运行地址。两个地址如果映射关系处理不好,镜像里的绝对地址引用就会出错。这种问题在开了地址无关编译选项后会缓解,但调试阶段我见过太多人栽在这上面,症状是 BL2 打印完第一行日志就死。

2.2 早平台初始化、架构初始化和外设初始化的先后顺序

BL2 的 C 初始化分三个阶段,分别对应三个回调:bl2_early_platform_setup、bl2_plat_arch_setup、bl2_platform_setup。这三个名字看起来相似,职责却完全不同。

bl2_early_platform_setup是最早被调用的 C 函数,这个阶段 MMU 还没开,跑在物理地址上。你要在这里初始化串口,否则后面出错没有任何打印。还要初始化存储控制器,因为 BL2 马上要访问 FIP 所在介质。这个函数不能碰复杂逻辑,它的核心目标就是"能打印、能读存储"。

bl2_plat_arch_setup紧接着被调用,负责建立 MMU 映射并开启 MMU。TF-A 对这块的要求是:BL2 能访问的安全内存区域、外设区域、FIP 映射区域,都要在这时候配置好。很多平台在这里使用mmap_add_region一个块一个块地加映射,顺序和权限都需要仔细核对。

最后的bl2_platform_setup是在所有镜像加载认证完成之后才调用的。为什么把外设初始化放在那么后面?因为 BL2 的安全策略是"最小化执行时间窗口",越快完成镜像认证和加载,就能越早把控制权交给 BL31。前面把不必要的外设都初始化了,反而扩大了攻击面。

2.3 堆大小不够导致证书解析失败的案例

BL2 在进行证书解析和签名验证时,需要一块动态内存。在 TF-A 里,这块内存由平台堆提供,大小通过BL2_HEAP_SIZE或者更细的宏来定义。很多平台的默认值在开发阶段够用,一旦你把算法从 RSA2048 换成 RSA4096 或者 ECDSA 和 RSA 混合,堆空间可能就不够了。

我实际遇到过的问题是:安全启动打开后,BL2 打印ERROR: BL2: Failed to load image,后面跟着一行 mbedTLS 的malloc failed。那时候第一反应是看镜像对不对、证书有没有问题,排查了一圈才发现是BL2_HEAP_SIZE设置得太小。mbedTLS 在做 RSA 私钥/公钥解析的时候,内存开销和密钥长度强相关,证书链越深、算法越复杂,堆用量涨得越快。

建议做法是给 BL2 堆预留至少 8KB 到 16KB 的余量,具体数值用实际业务跑一遍后再收紧。不要一开始就抠到极限,省那 2KB 内存换来的是一整晚的定位时间。

如果你遇到的是cert_parse相关的错误,先别急着怀疑算法实现,用 DEBUG 级别打开 TF-A 编译,然后看堆剩余空间和哪个分配器停了。安全启动里 80% 的"神秘错误"都是资源不足导致的,不是逻辑问题。

3. 认证框架:BL2 完成安全启动的核心机制

如果说前面的平台初始化和内存布局是安全启动的骨架,那认证框架就是安全启动的心脏。BL2 的所有价值几乎都体现在这里:它如何验证一个镜像是合法的、如何防止回滚、如何保证信任根不被伪造。

3.1 先分清 UEFI Secure Boot 和 ATF TBB 的差异

很多同学第一次接触"安全启动"这个概念是在 PC 上,Windows 报 BitLocker 蓝屏、提示安全启动被关闭,那个是 UEFI Secure Boot,属于固件管理层的启动验证。ATF 里的 TBB(Trusted Board Boot)是更底层的 SoC 固件信任链,它从 BootROM 开始,一级一级往下验证。两者名字都带 Secure Boot,但层级完全不同。

ATF TBB 的信任链大致是这样:SoC 内部 BootROM 是隐式的信任根,它验证 BL1 或者直接验证 BL2;BL2 再验证 BL31、BL32、BL33。每一级只需要信任上一级给它的公钥或者摘要。这个链条只要有一环是干净的,就能保证后续所有环节都来自可信的代码。

这个区别很重要,因为调试思路完全不同。PC 上的 Secure Boot 问题大多数是证书配置或者 BIOS 设置项问题,而 ATF TBB 问题往往是密钥、证书、FIP 打包方式三者之间任何一处不匹配导致的。

3.2 COT 描述符:把信任链用数据结构显式画出来

TF-A 的信任链不是写死在代码逻辑里的,而是通过一组描述符表(Chain of Trust)来定义的。这个设计非常优雅:你要新增一个平台镜像,不用改动认证框架本身,只需要在 COT 表里加一个条目。

COT 表的元素类型是auth_img_desc_t,每个元素描述一个待认证的镜像或者证书。拿 BL31 镜像来举例,它的描述符大概长这样:

static const auth_img_desc_t bl31_image = { .img_id = BL31_IMAGE_ID, .img_type = IMG_RAW, .parent = &soc_fw_content_cert, .img_auth_methods = { [0] = { .type = AUTH_METHOD_HASH, .param.hash = { .data = { .type = AUTH_PARAM_TYPE_ID, .cookie = (void *)SOC_FW_CONTENT_CERT_HASH }, .digest = { .type = AUTH_PARAM_TYPE_ID, .cookie = (void *)BL31_HASH } } } } };

这个结构的核心是parent字段,它显式指定了当前镜像的上级证书。BL31 的 parent 是 SoC FW Content Certificate,而这个证书的 parent 又是 Trusted Key Certificate,Trusted Key Certificate 的 parent 是 ROTPK。这么一路挂下去,最终挂到信任根。

认证框架本身不关心你的信任链长什么样,它只负责按照描述符的类型和顺序执行对应操作。这种解耦让同一份 BL2 代码可以适配完全不同的平台安全策略。

3.3 证书解析与签名验证:一个 BL31 镜像的认证全流程

以 BL31 镜像为例,BL2 执行认证的完整路径大概是这样的:

首先,BL2 通过 img_parser 模块在 FIP 包中找到 BL31 镜像的位置。这一步不涉及任何密码学操作,只是按 UUID 在 FIP 头部索引里查一下。

接下来,认证框架检查 BL31 的 parent 证书是否已经通过认证。如果没有,就递归先去认证 SoC FW Content Certificate。认证这个证书需要用到 Trusted Key Certificate 里的 Trusted World Public Key,于是又去认证 Trusted Key Certificate,最后用 ROTPK 验证 Trusted Key Certificate 的签名。这个递归过程保证了信任链initiation顺序从根到叶子。

等 parent 证书都认证完了,BL2 从 SoC FW Content Certificate 的扩展字段里提取 BL31 的摘要值,然后对加载到内存里的 BL31 镜像计算 SHA256(或者平台配置的摘要算法),两个值比对一致,镜像才算通过认证。

签名验证本身用的是标准 X.509 证书解析流程,TF-A 内部通过 crypto_mod 抽象层接入不同实现,默认是 mbedTLS,也可以换成硬件 CryptoCell。你在代码里看到的auth_mod_verify_signature、crypto_mod_verify_hash这些函数,就是这一整套流程的入口。

值得一说的是,BL31 镜像本身并没有独立的签名文件,它的签名信息是放在内容证书里的。为什么这么设计?因为镜像可能很大,给镜像整体签名再附加签名值,对 FIP 体积和存储带宽都是浪费。而证书里已经包含了镜像的哈希,这个哈希被证书签名保护,所以验签一次证书,等价于保护了镜像内容。这个设计在工程上很聪明,也是读代码时需要理解的关键点。

3.4 ROTPK 的存放:fuse 还是编译期绑定

ROTPK(Root of Trust Public Key)是整条信任链的锚点。它本身不参与验证,而是用来验证 Trusted Key Certificate 的签名。如果攻击者能把 ROTPK 改写,整个信任链就崩塌了。

TF-A 里 ROTPK 有两种落地方式。第一种是把 ROTPK 的哈希烧进 eFuse/OTP,平台通过plat_get_rotpk_info返回从 OTP 读到的 ROTPK 哈希。第二种是开发模式,通过ARM_ROTPK_LOCATION=devel把 ROTPK 直接编译进固件里,方便调试。

量产产品必须走 OTP 方案。fuse 烧错之后基本没有办法恢复,最稳妥的做法是先读回验证一遍再烧写下一步。开发阶段如果直接开 OTP,一旦密钥轮换或者生成错了,芯片就废了大半。

实际操作中我习惯在开发环境用 devel 模式验证整个启动流程,确认镜像、证书、FIP 打包都没问题之后,再切到生产模式做一次完整的 OTP 烧录回归测试。这样既能快速迭代,又不会在量产阶段被 fuse 问题卡住。

3.5 NV 计数器与回滚保护

防回滚是安全启动里容易被忽略但又极其重要的一环。攻击者不需要破解你的证书,他只要拿到一个旧版本的合法固件,重新刷回去利用已知漏洞,就能绕过新版本的安全修复。NV 计数器就是用来堵这个洞的。

TF-A 的机制是这样:每个内容证书里有一个sw_version字段,OTP/fuse 里存着一个 NV 计数器。BL2 验证证书时,会比较证书里的sw_version和计数器当前值。如果sw_version小于计数器,说明这是个旧版本,认证直接失败。如果sw_version大于计数器,认证通过后将计数器更新为新的sw_version。

这个机制写起来简单,落地时坑很多。最常见的是 NV 计数器位数不够用,特别是 fuse 位很稀缺的芯片,可能只留了 8bit 计数空间,版本号超过 255 就直接冲突。所以在设计证书版本规划时,要提前考虑好版本递增策略,不能随手往上加。

还有一类坑是多个镜像共享同一个 NV 计数器,导致一个镜像升级后其他旧版本的镜像全部失效。选型阶段就要确认平台支持几组 NV 计数器,Secure world 和 Normal world 各用哪个,别等产品快量产了再发现计数器不够分。

4. 从镜像加载到交棒 BL31:BL2 的收尾动作

BL2 的使命不是一直跑下去,它把 BL31 验证好、安排好内存、准备好参数之后,就要把执行权交出去。这个收尾过程本身也有不少细节,踩过坑的人都知道,交棒这一步如果参数没配对,BL31 起来第一件事就是死给你看。

4.1 FIP 与 IO 框架:BL2 怎么在存储介质里找到镜像

BL2 要加载的 BL31、BL32、BL33 以及各种证书,全部打包在一个叫 FIP(Firmware Image Package)的文件里。FIP 的头部是 TOC(Table Of Contents),每一项记录了一个镜像的 UUID、偏移、大小和标志位。BL2 通过 UUID 来区分要找的是哪个镜像,而不是通过文件名,因为在这个阶段根本没有文件系统的概念,UUID 就是它的文件名。

TF-A 的 IO 框架把不同存储介质抽象成了统一的接口。不管 FIP 放在 eMMC、NOR Flash 还是内存映射设备里,BL2 都通过io_open、io_read、io_close这一套 API 访问数据。这个抽象层让平台适配变得简单,但也带来一个问题:有些驱动在io_open阶段就会做大量初始化,如果介质本身没准备好,错误日志会出现在很靠前的位置,容易误导排查方向。

一个典型的排查场景是:FIP 打包顺序变了或者某个镜像的 UUID 写错了,BL2 打印找不到对应镜像,但你在 FIP 里又确实能看到文件。这时候用fiptool info xxx.fip查一下实际 UUID,再和平台的bl_common.h里定义比对,通常一两分钟就能定位。

4.2 bl2_main 的调用顺序:加载、认证、组装参数

bl2_main是 BL2 的 C 入口主函数,它的调用顺序就是 BL2 生命周期的缩影。以 TF-A 2.x 的经典实现为例:

  1. bl2_plat_preload_setup():平台预加载初始化。
  2. auth_mod_init():初始化认证模块,注册 crypto 驱动和证书解析器。
  3. img_parser_mod_init():初始化镜像解析模块。
  4. bl2_load_images():按 COT 表依次加载并认证所有需要启动的镜像。
  5. bl2_platform_setup():平台外设初始化。
  6. bl2_plat_get_next_bl_params():组装 BL31/BL32/BL33 的启动参数。
  7. bl2_plat_set_bl31_args():把参数写入约定的寄存器或结构体。
  8. bl2_run_next_image():跳转到 BL31。

bl2_load_images内部对每个镜像都会执行load_auth_image。这个名字起得很精准:既是加载又是认证。内部流程是先通过 IO 框架把镜像读入一块临时缓冲,然后调用认证框架做完整性校验,校验通过后才把镜像搬运到最终运行地址。为什么要分两步?因为镜像不能未经验证就写到运行地址,万一认证失败,你还能保证目标内存区域是干净的。

4.3 bl2_run_next_image 交棒:寄存器约定与新版本 Transfer List

bl2_run_next_image是 BL2 的最后一个关键动作。在经典实现里,BL2 会把bl2_params结构体的指针放到寄存器里,然后通过汇编el3_exit跳转到 BL31 的入口地址。BL31 入口代码会从寄存器里取出这些参数,继续后续启动。

这里有一个容易踩的坑:BL2 和 BL31 之间的参数传递依赖寄存器约定,如果平台缓存策略没处理好,BL2 写入的结构体数据还在 cache 里,BL31 从内存里读到的可能是旧数据。所以交棒之前通常要做 cache clean,确保数据落到了内存。这个问题在高优化级别编译下更容易出现,手动调试时很难复现,一开优化就崩。

新版本 TF-A(2.7 之后)逐渐把参数传递迁移到 Transfer List 机制,用链表结构组织各类配置数据,相比传统的固定结构体更灵活。BL2 阶段还会传递tb_fw_config这类的 FDT 配置块,BL31 启动时从中解析平台配置。如果未来你要做多级镜像的定制,建议优先看新版本 Transfer List 的实现,不要在新平台上硬套老版本的 bl2_params 结构。

4.4 安全启动失败时的日志定位思路

安全启动排查最怕的就是一上来就开 DEBUG 然后海量打印里乱翻。我的习惯是先看 BL2 最后一行有效日志,判断到底死在哪个阶段。

常见日志特征和对应原因大概这样:

日志特征大致原因
BL2: Failed to load imageFIP 缺少对应镜像或 UUID 不匹配
Authentication failed证书或签名验证没过,检查密钥链
Invalid certificate证书解析失败,检查证书类型或摘要算法
cert_parse相关错误堆内存不足或证书格式异常
BL2: Booting BL31之后死掉BL2 交棒成功,问题在 BL31 侧

实操排查我一般按这个顺序走:先用fiptool info核对 FIP 内容和预期是否一致;再确认当前固件使用 devel 还是 prod ROTPK;然后检查 NV counter 和证书的sw_version是否匹配;最后单独编译一个TRUSTED_BOARD_BOOT=0的版本,确认非安全启动下系统能正常跑起来。如果非安全启动正常、安全启动失败,那问题基本锁死在认证链路里。

我在实际项目里有个体会:安全启动调试花的时间,七成不是花在密码学上,而是花在"证书和镜像不匹配""FIP 打包顺序错""密钥没对上"这类工程问题上。所以先把工具链理清楚,比盲目看代码有效率得多。

排查安全启动问题多了以后,最大的心得其实是:把 TBB 当成一条数据流来看,而不是一堆密码学公式。BL2 的每个阶段无非是"从哪拿数据、用什么验证、验证过了往哪放"。搞清了这三个问题的答案,就等于给整条信任链画出了完整的执行路径。剩下需要做的,就是顺着这条路,一步一步核对你的 FIP、密钥、证书和内存布局是否匹配。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询