☰
深入ATF BL2启动流程:安全启动、镜像验证与调试指南
2026/10/2 22:12:14 网站建设 项目流程

上一篇文章里,我们把BL1的冷启动链路捋了一遍,重点看了它如何作为信任根完成最初的镜像验证。这次我们把视角往前推,专门盯住BL2。如果说BL1是看门老大爷,那BL2就是拿着清单逐个核对手续的安检员:它要在DDR能用之前搞定基本平台环境,要从FIP容器里把BL31、BL32、BL33一个个拖出来验明正身,最后还得干干净净地把控制权交给下一个阶段。

很多朋友看ATF的资料,看到bl2.bin、FIP、X.509证书、TRUSTED_BOARD_BOOT这些名词就头大,其实捋顺了也就那几件事。这篇会照着代码路径把BL2的启动过程拆开,再用一份实际编译、运行日志作为参照,把安全启动里最容易踩的坑挨个说一遍。适合正在做SoC底层启动、ATF移植,或者对可信固件感兴趣的朋友参考。

打个比方,Windows蓝屏时可能会提示“上次启动失败,安全模式可以帮助你解决问题”,嵌入式设备虽然没有这种交互式修复,但ATF的异常处理思路是类似的:任何一环验证不过,就拒绝继续启动。这个思想跟转录组分析流程里的质控步骤也很像,先过滤低质量样本,再往下走差异分析,而不是拿到数据直接跑计算。BL2在整个安全启动链里就是那个“质控关卡”,我后面会反复用这个类比来串场。

1. 先从全局看BL2:信任链上的“中转站”

1.1 从BL1到BL3,BL2为什么不可替代

ARM的可信固件ATF(现在官方叫TF-A,但大家还是习惯叫ATF)把启动过程分成了几个阶段:BL1、BL2、BL31、BL32、BL33。BL1是固化在片上ROM或者最小SRAM里的引导代码,是整个信任链的根;BL2则是第一个能从外部存储设备读取内容并做认证的“大逻辑”代码;BL31是EL3运行时固件,处理安全世界和普通世界之间的切换;BL32是可选的可信OS;BL33通常就是我们熟悉的U-Boot或者裸机程序。

安全启动的核心逻辑是信任链传递:BL1用片上预先烧录的公钥验证BL2的签名,通过后进入BL2;BL2再用自己持有的平台公钥验证BL31、BL32、BL33的证书和镜像;BL31运行后又会接管后续的启动认证。所以BL2是整个传递链上最关键的一个“中转站”。如果没有BL2,BL1就得自己完成DDR初始化、FIP解析和所有镜像验证,这对ROM里的代码量来说基本不现实。

用GEO数据挖掘那套流程来类比:下载数据以后,你得做数据清洗、标准化、质控,剔除掉异常样本,最后才能做差异分析。BL2做的事情就相当于“数据清洗+质控+差异分析”三合一:把FIP容器里的镜像解出来,逐一校验签名,再把真正可信的镜像送到指定的内存地址去执行。做错了任何一步,后面分析出来的结果都是垃圾。

1.2 BL2的代码入口与运行环境

BL2的代码主体在ATF源码的bl2/目录下,入口是汇编文件bl2/bl2_entrypoint.S,紧接着进入bl2_main.c执行主逻辑。编译生成的产物是bl2.bin或其带头版本的bl2.bin,最终会被打包进FIP。

BL2运行在EL1或者EL2(不同平台配置不同),但大多数情况下它的地址空间仍然限制在SRAM里。这是因为BL2执行早期阶段DDR还没有完成初始化,无法访问大内存。所以在代码里你会看到BL2_BASE、BL2_LIMIT这类宏,它们定义了BL2可用的SRAM范围。当BL2需要搬运比较大的BL33镜像时,才会先把DDR训练出来,然后把镜像从FIP所在存储介质复制到DDR中。

也因为BL2只能活在SRAM里,代码体积和栈空间都很敏感。我见过不少平台在BL2里塞了太多调试打印,直接把BL2_BASE和栈顶挤到一起,导致一进平台初始化就stack overflow。后续我们会聊到具体怎么排查这类问题。

2. BL2启动阶段逐步拆解

2.1 冷启动交接:BL1如何把控制权交给BL2

先看BL1是怎么结束的。在BL1完成镜像加载和验证后,它会调用bl1_run_bl2,这时会准备好跳转参数,然后跳到BL2的入口地址。BL2入口处的.macro el3_entrypoint_common会完成几个关键动作:设置当前CPU的异常级别、初始化栈、保存BL1传过来的内存信息参数,然后关闭中断并进入C环境的初始化。

这里有三个实际调试中容易忽略的点。

第一,BL1传给BL2的meminfo结构非常关键,里面记录了BL1自己占用的内存区域。BL2后续分配堆栈、加载镜像时要用这个信息避开BL1的领地,否则会把还在起作用的BL1代码和数据覆盖掉。

第二,入口汇编里有一部分是bl2_run_next_image最终要用到的基础上下文,比如x19、x20这些寄存器会保存bl31 entry point等关键信息。如果在早期初始化阶段误改了这些寄存器,后面跳转的时候会直接异常。

第三,要注意BL1加载BL2时用的镜像格式可能是带头的(比如BL2_IMAGE类型),BL2入口的.info被链接脚本和头部偏移所定义,不是简单的从头开始执行。建议在bl2_entrypoint.S的入口处打一个trace,确认首地址和预期一致。

2.2 平台初始化三连:early_platform_setup、arch_setup、platform_setup

进入C环境后,BL2会依次调用三个平台钩子函数。这三个函数是所有ATF移植工程师最早接触的东西,也在plat/目录下对应的*_bl2_setup.c文件中实现。

第一个是bl2_early_platform_setup。在这个函数里要做的最基础事情是初始化串口控制台。如果这个函数执行失败或者没有正确配置UART引脚,后续所有NOTICE、INFO、ERROR日志都看不到,整个启动过程就是“黑盒”。我建议在这个函数入口就立即打印一行固定字符,比如\r\nBL2_EARLY\r\n,这样可以快速确认控制台链路是否正常工作。它还会初始化系统计数器、中断控制器等基础设施,并解析BL1传过来的内存信息。

第二个是bl2_plat_arch_setup。这个函数负责构建BL2自己的页表,使能MMU和缓存。有些平台会在这里打开D-Cache以加速后续镜像拷贝,但如果页表映射不当,开了缓存反而会出现内存一致性问题。所以如果你在新的SoC上跑BL2,第一次建议先保持MMU off或者只映射SRAM区域,跑通后再逐步打开缓存。

第三个是bl2_platform_setup。这个阶段会做更完整的平台初始化,包括开启电源域、初始化时钟、配置DDR控制器等。它也是bl2_main里比较可能耗时的地方,很多厂商会把DDR训练代码挂在这里。需要注意,这个函数运行时代码仍在SRAM里,但数据可以写到DDR吗?不一定,如果DDR训练好了就可以访问。所以平台模板里通常会把DDR初始化放在这里。

这三个函数虽然名字相似,但执行环境和目的完全不同,我见过有伙伴把bl2_platform_setup里的外设初始化直接挪到arch_setup里,结果因为MMU未开启、外设是IO地址没有映射,一访问就同步异常。

2.3 DDR初始化和内存布局规划

BL2开始做正事之前,必须先保证BL33这种大镜像有地方放。DDR初始化是BL2的“体力活”,常见做法是调用厂商提供的DDR驱动API,比如ddr_init(),ddr_training(),然后等待校准完成。

这里有个地方要区分:并不是所有平台都把DDR初始化放BL2。有些平台的BL1阶段就能初始化DDR,目的是让BL2可以直接加载到DDR上运行。比如全志某些平台的BL1就完成了DRAM初始化。如果你的平台是从BL2才开始初始化DDR,那要特别注意DDR PHY校准期间的中断问题,有些DDR训练代码会长时间占住CPU,如果这时来了一个未屏蔽的串口中断或定时器中断,可能导致校准流程被干扰。

内存布局规划主要在bl2_plat_handle_post_image_load里完成。BL2加载完一个镜像后,会根据镜像ID把它放到对应地址。典型配置是BL31放SRAM或DDR起始区域,BL32放在安全DRAM,BL33放在普通DRAM。要让这些地址不重叠,需要对着SoC的内存映射表和链接脚本一个个核对。

我遇到过最离谱的一次是BL2镜像加载地址和加载后的目标地址重叠,导致BL2从FIP读数据到内存时,把还没读到的FIP头部给覆盖了,后续解析失败。后来我在plat_get_next_bl_params里强制把目标地址挪了1MB才解决。这类问题最好在早期设计时就画清楚内存图,不要等到跑崩了再猜。

2.4 按顺序加载镜像:FIP是什么,BL2怎么读

BL2的加载逻辑核心是load_auth_image,它会按预设顺序从FIP里取出镜像,并做认证。FIP可以看作一个合并了多个镜像和证书的容器文件,它的头部叫ToC(Table of Contents),记录了所有内容的UUID和偏移。BL2通过I/O驱动(比如flash接口)读取FIP头,然后根据镜像ID逐条查找。

默认的加载顺序是:先加载BL31,因为BL2最终要把控制权交给BL31;如果配置了TSP/OP-TEE,那么在BL31之后会加载BL32;最后加载BL33,即U-Boot或grub之类。

每个镜像有一个UUID,这在FIP内部作为索引。如果FIP里缺少某个镜像,BL2会报Failed to load image。如果镜像有了但签名不对,就会报Failed to authenticate image。这两个信息虽然很像,但含义完全不同。

加载成功后,BL2会把控制权交给BL31。它实际执行的是bl2_run_next_image,这个函数会先缓存要跳转到的entry point信息,然后执行一系列cache clean操作,确保镜像内容真正写到内存中。然后它切到EL3异常级别,把参数传递给BL31,完成交接。这一步如果做了安全启动验签,那么镜像数据虽然有很可能被cache缓冲,但在clean之后,BL31拿到的数据一定是经过认证的那份。

3. 安全认证机制:一块镜像怎么才算“可信”

3.1 认证框架与证书链

BL2的验签不是自己拍的脑袋,它用的是ATF的认证框架(Authentication Framework),这个框架支持X.509证书、RSA/ECDSA签名、哈希校验等。默认的证书链模型COT(Chain of Trust)大致是:RoT公钥(烧在ROM/OTP)验证BL2的信任证书,BL2的信任证书里包含BL2公钥,然后BL2用这个公钥验证自身镜像;接下来BL2用平台信任证书和BL31证书来验证BL31的镜像,BL32、BL33同理。

你可以把它想象成一层层授权:根密钥就是公司公章,BL2证书是部门开具的介绍信,BL31证书是具体办事人员的工牌。每一层都通过上一级的公钥验证下一级的证书签名,证书里存着下一级公钥。这样只要根公钥不失守,整条链就是可信的。

在编译期,cert_create工具负责生成这些证书和密钥。典型运行后会在输出目录生成rotpk.bin、trusted_key.crt、bl2.crt、bl31.crt、bl32.crt、bl33.crt等文件。其中rotpk.bin是根公钥的纯数据,需要烧写到SoC上。SoC中BL1(通常是ROM)会用这个根公钥去验证BL2的证书,因此如果重新生成了密钥但没更新烧录的rotpk,BL2永远验不过。

3.2 BL2对FIP内部镜像的验签流程

BL2拿到FIP中的镜像后,并不是直接校验整个文件。它先从FIP的ToC里找到对应镜像的UUID,读取镜像数据到临时缓冲区,然后根据认证参数去解析证书、提取公钥、执行RSA验签。

具体到代码上,load_auth_image会调用认证框架的auth_module_verify函数,这个函数会递归验证镜像的证书链。它会读取镜像周围的元数据(证书),提取签名值,再和计算出来的哈希做比对。这个过程中涉及RSA公钥操作和哈希计算,性能可能比较慢。在低端SoC上可能花几百毫秒,这是正常现象。

如果验证不通过,BL2默认会进入panic流程,打印错误信息后挂死或重启。在开启动WARMBOOT或RECOVERY机制较多的平台上,可能会回到BL1尝试下一份启动源。但不管怎样,受污染的镜像绝不会被跳转执行。

3.3 编译期如何开启TBB

开启安全启动通常推荐在编译时加上三个开关:TRUSTED_BOARD_BOOT=1、GENERATE_COT=1、MBEDTLS_DIR指向mbedtls源码目录。示例命令如下:

git clone https://github.com/ARM-software/arm-trusted-firmware.git git clone --branch mbedtls-2.28 https://github.com/Mbed-TLS/mbedtls.git cd arm-trusted-firmware make PLAT=qemu TRUSTED_BOARD_BOOT=1 GENERATE_COT=1 \ MBEDTLS_DIR=../mbedtls ARM_ARCH_MAJOR=8 \ LOG_LEVEL=40 all

编完后,在build/qemu/release/下面能看到bl1.bin、bl2.bin、fip.bin,还有keys/目录。keys/里是所有刚生成的私钥和证书,务必拷贝到安全位置保存好。如果这些私钥丢了,后续更新固件会非常麻烦,因为你没法签发新的FIP。

如果你不想要安全启动,只做普通ATF启动,就简单了:

make PLAT=qemu ARM_ARCH_MAJOR=8 all

但以“分析安全启动”为目的,肯定是需要开TBB的。注意开了TBB之后,BL1和BL2的体积都会变大,因为认证框架和证书解析代码加了不少。确实见过因为多加入代码导致BL2超出SRAM空间,平台编译直接报错的案例,这种时候要检查BL1/BL2的链接脚本,必要时裁剪掉不用的调试功能。

4. 实操记录:在QEMU/FVP上观察BL2安全启动日志

4.1 准备环境和编译

在写这个系列文章的时候,我实际用的环境是Ubuntu 22.04,配上aarch64-linux-gnu-gcc交叉编译器。确认编译器装好后,拉取源码、编译。

sudo apt install gcc-aarch64-linux-gnu make git clone https://github.com/ARM-software/arm-trusted-firmware.git git clone --branch mbedtls-2.28 https://github.com/Mbed-TLS/mbedtls.git cd arm-trusted-firmware make PLAT=qemu TRUSTED_BOARD_BOOT=1 GENERATE_COT=1 \ MBEDTLS_DIR=../mbedtls ARM_ARCH_MAJOR=8 \ LOG_LEVEL=40 all

这里我用的是qemu平台,因为它对TBB的支持比较完善,用FVP(固定虚拟平台)也可以,但qemu更容易在本地跑起来。如果你的环境中没有安装qemu-system-aarch64,还需要:

sudo apt install qemu-system-arm

4.2 集成FIP并运行

编译完成后,得到build/qemu/release/fip.bin和build/qemu/release/bl1.bin。在qemu中运行,最关键的是要让BL1能找到FIP。通常用pflash方式挂载FIP:

qemu-system-aarch64 -M virt -cpu cortex-a57 \ -nographic \ -bios build/qemu/release/bl1.bin \ -drive file=build/qemu/release/fip.bin,if=pflash,format=raw

如果你的ATF版本较新,可能还需要-S加上-s配合gdb调试,但只看启动日志不用那么复杂。执行后,串口会输出类似下面的信息:

NOTICE: Booting Trusted Firmware Boot Loader NOTICE: BL1: v2.9(release):v2.9(release) NOTICE: BL1: Built : 16:31:20, Feb 12 2025 NOTICE: BL1: Booting BL2 INFO: BL2: v2.9(release):v2.9(release) NOTICE: BL2: Built : 16:31:20, Feb 12 2025 INFO: BL2: Loading image id 1 INFO: BL2: Loaded image id 1 INFO: BL2: Loading image id 2 INFO: BL2: Loaded image id 2 INFO: BL2: Jumping to BL31

这里的image id 1通常代表BL31,id 2代表BL32或BL33,具体顺序要看平台配置。日志里没有出现“Failed to authenticate”就说明安全校验通过了。

4.3 日志关键字解读

为了方便排查,我整理了一张常见日志关键字速查表:

日志内容含义下一步建议
NOTICE: BL2: Loading image id 1开始加载BL31正常流程
ERROR: BL2: Failed to load image镜像无法加载,FIP中找不到检查FIP打包是否正确,fiptool dump
ERROR: BL2: Failed to authenticate image镜像验签失败检查key是否匹配,rotpk是否正确烧录
PANIC in BL2出现不可恢复异常开启LOG_LEVEL=50,查看panic前的原因
WARNING: BL2: Low entropy随机熵不足验证密钥时可能不够安全,关注TRNG
INFO: BL2: Jumping to BL31跳转成功后续看BL31日志

如果你看不到任何输出,先用最简单的bl1(不带TBB,不带FIP)跑一次,确认qemu命令行和编译配置没问题。如果能看到BL1日志但卡在BL2,多半是BL2串口驱动没初始化好,或者BL2镜像本身没被加载到正确地址。

4.4 在真实开发板上的差异

QEMU跑通只是第一步,从虚拟平台换到真实SoC,主要变化在于I/O驱动和DDR初始化。BL2的I/O驱动在QEMU里可能只是一个模拟flash,而真实板子上是NAND、eMMC或SD卡,而且往往需要板级初始化才能访问。

实践上,我建议分两步走。第一步还是关掉TBB,先把BL2能在真实板子上跑通,打印出完整的加载日志;第二步再打开TBB,把证书链加上。这样可以避免安全启动引入了额外复杂度,导致你分不清是驱动问题还是验签问题。这也是为什么很多厂商的参考板默认固件里先跑非安全启动,量产时才打开完整信任链。

5. 典型问题速查:BL2安全启动调试经验

5.1 镜像验签失败的排查思路

最常见的错误就是Failed to authenticate image。出现这个错误,优先检查三个方向:

第一,看FIP中打包的镜像是否和编译时使用的密钥一致。很多人会为了省事,拿一个旧fip.bin继续跑,但代码已经重新编译过、证书也换了,导致镜像和证书不配对。用fiptool可以查看FIP中包含的文件与证书信息,具体命令如下:

./tools/fiptool/fiptool info build/qemu/release/fip.bin

第二,检查rotpk。BL1作为信任根,它的公钥来自SoC的OTP/efuse。如果你在PC上跑QEMU,模拟器会从FIP里或某个固定地址读rotpk;在真实SoC上,如果熔丝和证书不匹配,同样验签失败。所以改密钥之前一定要确认能否同步更新OTP。

第三,看证书的有效时间。X.509证书如果设置了validity时间,而系统时钟错误,可能触发证书过期。不过ATF默认通常不做严格时间校验,这个概率较低,但也在实际项目里遇到过。

5.2 BL2 Panic最常见的五种原因

BL2 Panic是调试中比较难查的一类问题,因为它经常是“带崩了整个启动”。结合我的经验,给五个高概率原因。

第一,串口控制台初始化失败导致日志不可见,误以为Panic无输出。其实你要先确保UART波特率和引脚配置正确,最好在BL2早期打固定字符串。

第二,内存地址重叠。BL2的栈、堆、暂存缓冲区和要加载的镜像目标地址重叠,会在加载过程中踩坏数据。建议在链接脚本里确认BL2的栈底不越过BL2_LIMIT。

第三,DDR初始化失败。这类问题表现通常比较隐晦,因为DDR驱动往往会打印自己的训练日志,但BL2主循环可能因为时序问题读回全0xff。如果遇到加载BL33后校验总非法,优先检查DDR配置参数。

第四,电源域和时钟没配置好。在BL2阶段你访问的很多外设都还没有初始化,如果没有提前给对应模块开时钟,一读状态寄存器就触发Data Abort。确保bl2_early_platform_setup里打开了需要的外设电源。

第五,钩子函数实现有误。比如plat_get_next_bl_params里没有正确填充entry point信息,跳转参数全为0,进入BL31后自然panic。这类问题可以通过在跳转前打印next_bl_ep_info内容来定位。

5.3 BL1和BL2的证书链衔接问题

如果你发现“明明验签通过了,但BL31跑飞”,不要只盯着BL2代码,还得看BL1传给BL2的信任链状态。特别是在安全启动模式下,BL1首先验证BL2镜像,它使用的是一个固定的rotpk,这个rotpk通常来自SoC硬件。你在编译BL2时用的根密钥必须和rotpk一致,否则BL1连BL2都不会放行,日志表现为“BL1: image auth error”。

还有一个容易被忽略的点:开启动TBB后,BL1加载BL2的地址也要放在信任的内存区域。如果BL2代码被加载到非安全DRAM,BL1的校验结果就可能被篡改。因此SoC平台通常会强制把BL2放入SRAM,并用TZASC/TrustZone保护这片区域不被普通世界访问。

5.4 类比“安全模式”和生信流程:启动失败后的回退策略

前面提到Windows的“上次启动失败,安全模式可以帮助你解决问题”,在ATF的启动里也有一套类似机制。很多平台支持WARMBOOT,当BL2或BL31校验失败后,会回到BL1进入恢复模式,从USB或UART重新下载固件。这个恢复模式跟“安全模式”一样,都是在异常启动路径下提供一个可维护的环境。

做GEO数据挖掘的时候,我们也强调数据下载后先做质控,剔除低质量样本,再做标准化后的差异分析,不会把不合格样本直接代入下游分析。BL2的验签和恢复机制正是这种“不合格就不推进”的工程哲学,只不过它面对的不是转录组矩阵,而是固件镜像。理解了这个思路,你再看BL2的异常分支代码,就很容易明白哪些路径是死路、哪些路径是救援通道。

6. 个人经验与一点建议

最后分享几个实操层面的个人看法。

第一个建议是把BL2的日志当成第一优先级来对待。我在产品调试早期,经常遇到“黑屏死机”,结果发现是UART控制器的时钟树配置不对,导致串口迟迟没有输出。后来我在所有平台初始化钩子前加了一个汇编级的早期打印,每次上电都能看到第一个字符从哪里开始。这个习惯帮我省了很多排查时间。

第二个建议是“先不带TBB,后带TBB”。很多朋友一上手就开TRUSTED_BOARD_BOOT,结果验签失败后压根分不清是驱动问题还是证书问题。正确姿势是先关掉TBB,纯加载流程跑通,确认BL2能依次加载BL31、BL33并跳转;然后再打开TBB,纯验签链路单独测。两步合起来,才能快速定位是哪一个环节出问题。

第三点是务必管理好密钥生成环境。cert_create在编译时生成的密钥如果丢了,更新固件就只能重新烧bootrom。我的习惯是把keys/目录纳入公司内部密钥管理系统,固化产物包括证书、私钥、rotpk,并记录生成时间和构建机器哈希。量产阶段的密钥生命周期比任何代码都金贵。

BL2这部分内容还能继续往深处挖,比如BL31的运行时服务初始化、BL32的加载策略、以及安全启动在量产时的Key Provisioning流程,都是很好的延伸方向。后续如果再写这个系列,我会优先把BL31的冷启动交接讲透,因为那里有更多平台相关的地雷等着踩。

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

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

立即咨询