资讯详情

深入ATF BL2启动流程:安全启动、镜像验证与调试指南

📅 2026/10/2 22:12:26 | 华诺云谱 👁 阅读
深入ATF BL2启动流程:安全启动、镜像验证与调试指南
上一篇文章里我们把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到BL3BL2为什么不可替代ARM的可信固件ATF现在官方叫TF-A但大家还是习惯叫ATF把启动过程分成了几个阶段BL1、BL2、BL31、BL32、BL33。BL1是固化在片上ROM或者最小SRAM里的引导代码是整个信任链的根BL2则是第一个能从外部存储设备读取内容并做认证的“大逻辑”代码BL31是EL3运行时固件处理安全世界和普通世界之间的切换BL32是可选的可信OSBL33通常就是我们熟悉的U-Boot或者裸机程序。安全启动的核心逻辑是信任链传递BL1用片上预先烧录的公钥验证BL2的签名通过后进入BL2BL2再用自己持有的平台公钥验证BL31、BL32、BL33的证书和镜像BL31运行后又会接管后续的启动认证。所以BL2是整个传递链上最关键的一个“中转站”。如果没有BL2BL1就得自己完成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放在安全DRAMBL33放在普通DRAM。要让这些地址不重叠需要对着SoC的内存映射表和链接脚本一个个核对。我遇到过最离谱的一次是BL2镜像加载地址和加载后的目标地址重叠导致BL2从FIP读数据到内存时把还没读到的FIP头部给覆盖了后续解析失败。后来我在plat_get_next_bl_params里强制把目标地址挪了1MB才解决。这类问题最好在早期设计时就画清楚内存图不要等到跑崩了再猜。2.4 按顺序加载镜像FIP是什么BL2怎么读BL2的加载逻辑核心是load_auth_image它会按预设顺序从FIP里取出镜像并做认证。FIP可以看作一个合并了多个镜像和证书的容器文件它的头部叫ToCTable 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签名、哈希校验等。默认的证书链模型COTChain 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的证书因此如果重新生成了密钥但没更新烧录的rotpkBL2永远验不过。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_BOOT1、GENERATE_COT1、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 PLATqemu TRUSTED_BOARD_BOOT1 GENERATE_COT1 \ MBEDTLS_DIR../mbedtls ARM_ARCH_MAJOR8 \ LOG_LEVEL40 all编完后在build/qemu/release/下面能看到bl1.bin、bl2.bin、fip.bin还有keys/目录。keys/里是所有刚生成的私钥和证书务必拷贝到安全位置保存好。如果这些私钥丢了后续更新固件会非常麻烦因为你没法签发新的FIP。如果你不想要安全启动只做普通ATF启动就简单了make PLATqemu ARM_ARCH_MAJOR8 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 PLATqemu TRUSTED_BOARD_BOOT1 GENERATE_COT1 \ MBEDTLS_DIR../mbedtls ARM_ARCH_MAJOR8 \ LOG_LEVEL40 all这里我用的是qemu平台因为它对TBB的支持比较完善用FVP固定虚拟平台也可以但qemu更容易在本地跑起来。如果你的环境中没有安装qemu-system-aarch64还需要sudo apt install qemu-system-arm4.2 集成FIP并运行编译完成后得到build/qemu/release/fip.bin和build/qemu/release/bl1.bin。在qemu中运行最关键的是要让BL1能找到FIP。通常用pflash方式挂载FIPqemu-system-aarch64 -M virt -cpu cortex-a57 \ -nographic \ -bios build/qemu/release/bl1.bin \ -drive filebuild/qemu/release/fip.bin,ifpflash,formatraw如果你的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通常代表BL31id 2代表BL32或BL33具体顺序要看平台配置。日志里没有出现“Failed to authenticate”就说明安全校验通过了。4.3 日志关键字解读为了方便排查我整理了一张常见日志关键字速查表日志内容含义下一步建议NOTICE: BL2: Loading image id 1开始加载BL31正常流程ERROR: BL2: Failed to load image镜像无法加载FIP中找不到检查FIP打包是否正确fiptool dumpERROR: BL2: Failed to authenticate image镜像验签失败检查key是否匹配rotpk是否正确烧录PANIC in BL2出现不可恢复异常开启LOG_LEVEL50查看panic前的原因WARNING: BL2: Low entropy随机熵不足验证密钥时可能不够安全关注TRNGINFO: 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代码被加载到非安全DRAMBL1的校验结果就可能被篡改。因此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的冷启动交接讲透因为那里有更多平台相关的地雷等着踩。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑