
最近在折腾自家板子的安全启动链路正好把SOC-ATF里BL2这一段的流程彻底捋了一遍。网上聊ATF启动流程的文章不少但大多停留在“BL1验BL2BL2验BL31”这种粗线条真正落到代码路径、认证框架、COT配置细节的不多。这篇文章算是系列第二篇承接之前对BL1和信任根的分析把BL2从入口到跳转下一阶段的完整流程拆开来讲包括镜像加载、认证、以及我实际调试中踩过的几个坑。适合谁看主要面向做固件开发、BSP移植、或者正在搞安全启动方案选型的工程师。如果你是刚接触ATF建议先跑一遍官方文档里的TBBRTrusted Board Boot Requirements概念再回来看代码会顺畅很多。1. 安全启动链路全景与BL2的定位1.1 从信任根到信任链很多人第一次接触ATF容易被“BL0、BL1、BL2、BL31、BL32、BL33”这一串名字搞晕。先理清一个核心概念安全启动的本质不是“某一个镜像本身可信”而是“从不可变的那一点开始逐步验证后续所有被加载的东西”这就是信任根RoT和信任链Chain of Trust的由来。在典型的ARM SoC里信任根通常是ROM里的代码也就是BL1或者被称为BootROM。它固化在芯片内部出厂后就不变了它用自己的公钥或者哈希值去验证BL2的镜像。BL2被验证通过之后再由BL2去验证BL31EL3 Runtime Firmware、BL32可选的可信OS比如OP-TEE、BL33通常是U-Boot或者UEFI。这样一来每一级只信任上一级验证过的东西整个链路就闭合了。这个设计里BL2的位置非常特殊它是Trusted Firmware主流程里第一个“可灵活配置”的镜像也是第一个由芯片厂商和OEM可以完全自主控制的固件。BL1通常很小能做的逻辑有限大部分真正的“启动策略”都压在了BL2身上。这也是为什么这两年在做安全启动落地的团队几乎都要在BL2上花大力气。1.2 BL2在启动流程中的职责边界具体到BL2这个阶段它的职责可以概括成三件事加载、认证、传递控制权。加载从存储介质里把后续镜像读进内存。存储介质可能是NOR Flash、SD/eMMC、USB、UART取决于平台实现。认证用COTChain of Trust里定义的认证方法验证这些镜像的完整性和来源。传递控制权认证通过后跳转到下一个阶段通常是BL31也可能先跳到BL32。这里有一个很多人容易忽略的点BL2的代码本身也分两段执行。它在BL1验证通过后一开始是在SRAM里跑的。但SRAM通常不够大所以BL2需要把自己的代码或者数据段放到DDR里这就涉及“BL2运行的地址重映射”问题。我见过不少团队在这块翻车——BL2里跑着跑着访问了非法地址整个系统直接挂掉。BL2的另外一个职责是负责形成“FIP”Firmware Image Package的解析。FIP是一个打包格式把BL31、BL32、BL33、证书等资源打成一个镜像文件方便烧录和管理。BL2从FIP里按ID取出需要的镜像然后交给认证框架处理。这个设计在量产场景里很好用因为换密钥或者换BL33版本只需要重新打包FIP不用动BL1和BL2本身。2. BL2核心启动流程拆解2.1 BL2入口与早期初始化以当前主流的ATF版本2.x为例BL2的入口是bl2_entrypoint这个符号在bl2/aarch64/bl2_entrypoint.S里定义。它做的最重要的事情是设置栈指针、保存通过寄存器传入的参数这些参数是BL1在跳转时传过来的包含内存信息、BL2镜像在内存中的位置等然后调用bl2_early_platform_setup2。bl2_early_platform_setup2是平台相关函数不同SoC的实现天差地别。最基本的任务包括初始化调试串口不然后面日志都看不到。解析BL1传过来的内存布局信息。配置平台相关的私密硬件比如TrustZone地址空间控制器。这一步是“早期初始化”不能做太多事情因为它可能还在SRAM里跑栈空间有限有些依赖DDR的模块还没就绪。之后会调用bl2_plat_arch_setup这一步主要是设置MMU和内存映射。ATF本身对MMU的管理非常简化不建复杂的页表但还是会创建基础的段映射保证后续代码能跑在虚拟地址上。这里要补充一句如果平台从SRAM切换到DDRMMU的映射一定要同步更新否则跳到DDR里执行的时候指令和数据都访问不到系统直接异常。2.2 镜像加载与解析完成基础初始化后BL2进入主逻辑bl2_main。这个函数在bl2/bl2_main.c里核心流程大概是bl2_arch_setup设置架构相关的安全配置。bl2_platform_setup完整的平台初始化此时大多数外设已经可用。bl2_plat_preload_setup可选有些平台需要在加载镜像前做额外准备。加载并认证BL32和BL33镜像。最后执行bl2_run_next跳转。其中最关键的是镜像加载。ATF定义了一套标准接口load_auth_image。这个函数做的事情是“一边加载一边做基础检查”。它并不会提前把整个镜像全读进内存再开始验证而是按块block读取先验证镜像头再根据头信息去加载剩余的body。这种设计的好处是节省内存尤其对SRAM空间紧张的芯片很友好。加载镜像时需要知道镜像源。ATF通过plat_get_image_source获取镜像存储的位置。例如从NOR Flash的偏移地址0x100000处读取FIP包那么BL2会解析FIP包结构找到目标镜像在FIP内的偏移再逐块拷到加载地址。这里有一个实操细节FIP包的解析有一套独立的解析器在drivers/io和drivers/io/io_fip.c里它把FIP当作一个虚拟设备。你在调试时如果遇到“找不到镜像”的报错先不要怀疑代码优先确认FIP包的烧录地址跟plat_get_image_source返回的地址是否一致。我遇到过的绝大多数此类问题都是地址不匹配而不是FIP本身损坏。2.3 镜像认证与授权校验镜像加载完成后BL2会进入认证环节。ATF的认证框架在drivers/auth目录下核心逻辑包括auth_mod认证模块统一处理各种认证方法。img_parser_mod镜像解析模块负责从不同格式的镜像裸二进制、证书中提取需要的信息。密码学操作通常是调用mbedTLS来完成比如RSA签名验证、SHA-256哈希计算。认证的大致流程是先根据COT配置确定每个镜像使用哪种认证方式。如果是“裸镜像”RAW_IMAGE常见的方式是哈希校验从证书里读出一个期望的哈希值然后对镜像本身计算哈希两者比对一致就通过。如果是“证书”IMG_CERT则需要先验证证书的签名再从证书里提取公钥或哈希用于下一步验证。很多人在这一步会困惑为什么BL2既要验证BL32/BL33又要验证证书其实这是一个“链式信任”的过程。比如BL31的镜像不是直接用一个固定的公钥去验证的而是先加载一张证书证书里包含BL31镜像的哈希值。验证这张证书的签名确实是由可信私钥签发的然后才信任证书里的哈希值再用这个哈希值去校验BL31的镜像。这样一来哪怕公钥很长、证书结构很复杂最终需要Trusted Root Key验证的内容尽可能少只需要验证一张“根证书”级别的签名即可后面全是哈希比对性能也能接受。关于哈希比对要留意ATF的固件实现。在认证过程中ATF会先把镜像按块读入边读边计算哈希全部读完后再跟期望值比对。这样做内存占用低但调试时如果哈希校验失败往往不好定位是数据读错了还是证书里的期望值本来就不对。这时我的经验是先手动算一遍FIP包里源文件的SHA-256再对比证书里记录的哈希如果这两个一致问题多半出在读取端比如存储介质不稳定或者读取地址偏移了。3. COT配置与密钥体系3.1 COT描述符框架COTChain of Trust在ATF里不是一个抽象概念而是一张静态配置表通常在plat/platform/board/board_cot.c或common/cot.c里定义。这张表用auh_img_desc_t结构体描述每一个“被认证的镜像”或“证书”的认证方式。static const auth_img_desc_t trusted_boot_fw_cert { .img_id TRUSTED_BOOT_FW_CERT_ID, .img_type IMG_CERT, .parent NULL, .img_auth_methods { [0] { .type AUTH_METHOD_HASH, .param.hash { .data raw_data, .digest tb_fw_hash, } } } };img_id对应镜像包里每个镜像的唯一编号img_type区分是证书还是裸镜像。parent表示这张证书的“上一级”证书是谁这就构成了信任链的层级关系。img_auth_methods是数组可以同时定义多个认证方法实际执行时逐个尝试全部通过才算认证成功。理解这个结构后你会发现ATF的认证框架其实是可配置的如果平台不想用RSA签名也可以用ECC密钥或者只做哈希完整性校验而不做来源认证。关键是修改COT表而不是改认证框架本身的代码。这一点对二次开发特别友好我见过有的团队把COT改成两级证书先验平台证书再验产品证书就是为了实现不同的产线权限分离。3.2 BL2认证BL31/BL32/BL33的过程以最常见的TBBR流程为例BL2在加载完镜像后的认证顺序大致是先认证TRUSTED_BOOT_FW_CERT也就是“可信固件证书”。这个证书里包含了BL31镜像哈希、BL32镜像哈希、以及BL33镜像哈希。这张证书的签名是用Trusted World KeyTWK签发的而TWK本身又是被根密钥ROTPK验证的。所以在加载这张证书之前BL2可能还需要先验证一个“平台密钥证书”之类的中间证书具体看平台设计。然后逐个认证BL31、BL32、BL33镜像。它们的认证方式基本都是AUTH_METHOD_HASH因为哈希已经在可信固件证书里了。这个过程中ROTPK的获取特别重要。TBBR里规定ROTPK要么烧录在OTP里要么以固定值编译进BL1/BL2里。如果是烧录OTPBL2通过读取OTP控制器获取公钥如果是编译进去那么公钥就是一个常量数组。使用OTP的好处是方便产品化换密钥但坏处是OTP本身的规划要提前做好位宽、ECC校验位、读取时序都可能踩坑。我在实际项目中遇到过一个比较典型的问题OTP里存的ROTPK读出来跟证书里包含的ROTPK哈希不一致导致认证一直失败。后来发现是OTP的读取接口返回的是“大端序”而证书里的哈希是按小端序计算的一个字节序转换没做查了一整天才定位到。这里提个醒涉及密码学数据字节序一定要形成一个固定的检查清单防止重复踩坑。3.3 密钥大小与签名性能取舍BL2做非对称签名验证时最常见的算法是RSA-2048和RSA-3072也有用ECDSA的。RSA验证慢但是支持度高、资料多ECDSA验证快、密钥短但在一些安全认证场景下合规性需要注意。在BL2阶段做签名验证性能是非常现实的问题。假设BL2运行在1GHz的CPU上用纯软件mbedTLS做一次RSA-2048验签大概需要几毫秒到十几毫秒具体取决于有没有硬件加速器。如果在低功耗MCU级别可能一次验签要几十毫秒甚至上百毫秒。这个时间在开机流程里可能不算什么但如果BL2需要验证多个镜像和证书累加起来就是几百毫秒某些有启动时间要求的场景是无法接受的。所以很多SoC会在硬件里集成加密引擎ATF也提供了对应的驱动接口比如ARM CryptoCell系列。如果平台有硬件加速记得确认platform_def.h里是否定义了ARM_CRYPTOCELL_INTEG之类的宏同时要确保mbedTLS的配置启用了硬件加速回调函数。否则即使硬件存在ATF还是会用软件实现白瞎了硬件性能。4. 定制与移植要点把BL2变成自己的4.1 添加自定义镜像支持如果你要在启动流程里增加一个自定义镜像比如一个独立的固件用于初始化ISP或者NPU那么至少要做三件事在镜像ID枚举里增加一个新ID比如NPU_FW_IMAGE_ID。在bl2加载阶段增加对应的加载和认证调用。最简单的做法是仿照load_bl32或者load_bl33写一个load_npu_fw然后用load_auth_image加载它。在COT表里为这个镜像添加对应的auth_img_desc_t描述并指定认证方式。如果只是完整性强校验可以把它作为AUTH_METHOD_HASH关联到某个已有证书上如果它是独立签名的就要生成新的证书和密钥体系。这里最大的坑是“加载顺序”。BL2默认的加载顺序是固定的先BL32、再BL33。如果你要加载的镜像依赖DDR已经被初始化那没问题但如果你的镜像需要更早运行比如要在BL31初始化之前配置某些安全寄存器那就得想清楚到底是放在BL2阶段加载并在BL2里执行初始化还是把初始化逻辑放到BL31的bl31_early_platform_setup里。很多时候把自定义逻辑放到BL31比放在BL2里更合理因为BL2本身生命周期太短它的状态在跳转之后就没了。4.2 密钥轮换与产线烧录BL2的密钥管理不仅仅是开发期的事量产阶段也要考虑。TBBR支持“多重密钥”模式即每个产品可以有一套属于自己的密钥而不是所有机器共用一套根密钥。实现方式是在OTP里保存ROTPK的哈希值同时证书链中包含ROTPK本身这样每台设备在验证时只要确认证书里ROTPK的哈希与OTP里存储的一致就能信任整条证书链。这种模式的好处是灵活但坏处是烧录复杂度增加。产线上烧录时需要先生成对应的密钥对和证书链再把ROTPK写入OTP最后打包FIP。任何一个环节乱序都会导致设备变砖。我建议量产工具一定加上“烧录前校验”的步骤在烧录完成后立刻读回OTP内容和FIP头签名结果做自检不要光写不看。另外一个容易忽略的点是BL2镜像本身的更新策略。BL2不像BL33那样能随便升级因为它是被BL1验证的BL1又不可变。如果你改了BL2就必须更新BL1里内置的BL2公钥或者采用“BL1允许加载多版本BL2”的设计。后者实现复杂早期规划时就要想清楚。5. 环境准备与调试技巧5.1 搭建最小化调试环境要想顺利分析BL2流程一个能稳定复现的环境至关重要。我个人的习惯是在QEMU或者FPGA原型平台上先跑通再切换到真实芯片。ARM官方有FVPFixed Virtual Platform模拟器配合ATF源码几乎可以模拟完整的启动链路非常适合验证COT配置是否正确、认证流程是否走得通。在QEMU或者FVP上调试日志是核心手段。ATF的日志等级通过LOG_LEVEL宏控制尽量把LOG_LEVEL调到LOG_LEVEL_VERBOSE50并且在plat/common/plat_common.c或平台特定的配置里确认LOG_LEVEL的宏没有被覆盖。同时建议把DEBUG选项打开make DEBUG1这样生成的固件会包含更多调试符号和断言信息。别小看这些断言ATF自带很多对结构体和内存范围的检查打开之后很多问题能提前暴露而不是跑着跑着死机。5.2 常见错误日志解读与定位BL2最常出现的错误日志是Authentication failed。这通常意味着某个镜像的签名验证或者哈希比对没有通过。定位思路建议按以下顺序排查先用源码里附带的fiptool查看FIP包中的证书和镜像哈希是否与预期值一致。命令类似fiptool info fip.bin fiptool unpack fip.bin解开FIP后可以用openssl x509 -text查看证书内容确认里面记录的镜像哈希是否正确。确认BL2加载到的数据没有问题。可以在load_auth_image调用后把镜像的内存地址和长度打印出来再对照代码里预期加载地址看有没有偏差。排除密钥不匹配问题。如果证书是用A密钥签的而BL2里烧录的ROTPK对应B密钥那么签名验证必然失败。这时要检查OTP或者board_cot.c里使用的密钥和证书是否是一套生成的。另一类常见问题是内存分配不足。BL2在SRAM里运行时堆栈和镜像加载缓冲区都是有限的。如果镜像超过预期大小可能出现数据被覆盖导致校验失败。遇到这种问题日志里往往不会直接提示“buffer overflow”而是表现为随机性的哈希校验失败或者跳转到异常地址。排查时要注意看bl2_plat_get_next_bl_params里设置的内存布局是否与链接脚本一致。5.3 我踩过的坑字节序与对齐前面提到字节序问题其实是我在真实项目里踩过的最大一个坑。当时移植一个新的安全启动方案证书验签一直失败但证书里的公钥和OTP里读出的数据裸眼看就是“一样的”。后来通过打印十六进制逐字节对比才发现OTP读出来是低位在前证书解析出来是高位在前因为ATF里不同模块对字节序的处理方式不完全统一。从那以后我给自己定了一个规矩所有涉及证书、公钥、哈希等二进制安全数据的读取和打印都统一先转成大小端明确的数组格式再做比对。BTW关于对齐问题也要留意。某些加密引擎要求输入数据按32字节或者64字节对齐如果你把证书地址直接指向一个未对齐的内存位置硬件引擎会直接报错或者行为异常。解决办法是在platform.h里给镜像加载缓冲区加上对齐属性而不是在每处调用点临时处理。6. 结尾一点实际操作体会把BL2流程真正跑通之后再去回头看TBBR文档和ATF源码会有一种“果然如此”的通透感。这个阶段虽然代码量不大但每一个细节都牵扯到安全边界的建立任何一步松懈都可能导致整个信任链崩塌。我个人的体会是分析BL2流程不能只盯着ATF代码本身还要结合具体的SoC平台手册、存储介质特性和产品应用场景来理解。比如同一套BL2逻辑在带可信执行环境的手机上和在简单的IoT设备上侧重点就完全不同。最后分享一个小技巧在调试BL2时如果条件允许尽量保留一个通过JTAG直接查看SRAM和DDR内容的通道。很多启动失败的问题单靠日志很难定位但只要能读出关键内存地址里的数据原因往往一目了然。安全启动是个系统工程BL2只是其中一环但把这一环吃透整个安全链路的信心就有了根基。