ATF源码深度评测与平台移植实战:从BL1到BL31的可信启动链解析
2026/9/8 5:25:21 网站建设 项目流程

站在系统软件和嵌入式安全的交叉路口,Arm-Trusted-Firmware(ATF)可能是大多数开发者既熟悉又陌生的存在。说熟悉,是因为几乎任何基于Arm架构的现代设备,从手机SoC到服务器CPU,启动流程里都绕不开它;说陌生,是因为真正翻开源码、把BL1到BL31的运行脉络理清楚的人并不多。这篇文章我想从工程角度做一次深度源码评测和平台移植复盘,聊清楚ATF的架构全景、固件安全工程的关键审计点,以及把ATF移植到一块新板子上时,踩过的坑和沉淀下来的方法。

这篇文章适合三类人:一是做BSP和bootloader开发的嵌入式工程师,二是做安全启动、可信固件方案选型的技术负责人,三是对Arm体系结构感兴趣、想弄明白“上电之后第一段代码到底干了什么”的进阶学习者。我会尽量用项目实战的口吻来讲,不堆概念,重点放在“为什么这么设计”和“实际操作时怎么落地”。

1. ATF架构全景:从BL1到BL33的一条完整可信链

1.1 启动流程里的四级跳

先把ATF在启动链路中的位置说清楚。Arm平台从上电到操作系统跑起来,通常会经历ROM代码、ATF的多个BL阶段、以及最终的EL2/EL1系统软件,ATF在这一链路中承担的是“安全世界的基石”角色。按标准术语,ATF包含BL1、BL2、BL31,而BL32(可选的安全世界OS)和BL33(通常是U-Boot或UEFI)也在整个可信启动框架中扮演重要角色。

BL1是上电后最早运行的ATF代码,通常固化在ROM或者BootROM中,负责最基础的CPU初始化、DDR初始化(早期阶段)和加载BL2。它必须足够小、足够稳,因为此时DRAM可能还没有准备好,代码通常在SRAM或内部RAM中运行。BL1完成使命后会跳转到BL2。

BL2是可信启动过程中的“加载器”,运行在Secure世界,负责从非易失存储(如eMMC、NOR Flash)加载BL31、BL32和BL33镜像,验证它们的签名和哈希,然后按顺序把它们放到约定的内存地址。BL2结束后会回到BL31。

BL31是runtime firmware,运行在EL3,提供runtime服务,比如PSCI(电源状态协调接口)、SMC(安全监控调用)处理和系统唤醒流程。BL31是最复杂的一块,它长期驻留内存,操作系统运行期间发生安全监控调用时,CPU会从非安全世界trap到EL3,由BL31的异常处理框架接管。

BL32是可选的可信OS,比如OP-TEE,提供安全世界运行环境。BL33则是正常世界的第一阶段bootloader,在服务器领域通常是U-Boot或UEFI,在移动平台可能是LK或ABL。整个链条可以看作一条“信任接力棒”:每一级验证下一级的完整性和来源,最终把控制权交给操作系统。

理解这条链的关键:可信根在BL1,安全边界在EL3,而BL31是所有非安全世界请求的“看门人”。ATF的设计哲学就是把安全关键代码收敛到最小可信计算基(TCB)内。

1.2 EL3和安全世界模型的关键设计

ATF运行在EL3,这是Armv8-A架构里权限最高的异常级别。EL3之下还有EL2(虚拟化)、EL1(OS内核)、EL0(应用)。这种分层设计的意义在于:普通世界运行的所有代码,包括内核本身,都无法直接访问EL3的资源和寄存器,必须通过SMC指令显式请求服务。

从源码角度看,ATF在EL3维护了一个SMC调度表。以标准PSCI调用为例,操作系统执行smc #0指令时,CPU会陷入EL3,BL31的smc_handler会根据x0寄存器中的SMC功能ID查表,路由到对应的处理函数。这个机制是ATF最核心的价值之一:它把电源管理、系统复位、安全配置等关键操作收拢到可信固件层,让内核和Hypervisor无法随意操作硬件,从体系结构层面避免“内核被攻破=设备被完全控制”的一锅端局面。

ATF还实现了Secure/Non-Secure世界之间的中断路由。一个典型场景:安全世界(比如TEE)在处理敏感数据时来了一个普通世界的中断,此时ATF会决定是先处理中断还是屏蔽中断。这个决策逻辑在源码中表现为interrupt_type标志和cm_el3_sysregs_context_save/restore这样的上下文保存恢复机制。这块内容很细,但如果要做安全固件审计,必须把中断路由和上下文切换的路径啃透,否则很容易在“安全世界被普通世界中断干扰”这类边角问题上翻车。

从工程视角看,整个ATF源码布局高度模块化,plat/目录存放平台相关代码,lib/目录存放通用库,drivers/目录收敛各种外设驱动。这种结构把“平台无关”和“平台相关”彻底切开,也是我们做移植时最依赖的分层逻辑。

2. 安全固件工程审计:ATF源码里的关键设计意图

2.1 可信启动链的“密码学接力”

做安全固件审计,最先要看的就是可信启动链。ATF的trusted boot并不是简单地把镜像加密或签名,它是把完整性度量、认证、解密分散到多个阶段,每一级只信任上一级交给它的东西。

以加载BL2为例,BL1在启动早期会利用平台提供的root-of-trust公钥(ROTPK)验证BL2镜像的签名。Arm标准的做法是使用X.509证书链:BL1首先验证BL2的证书,再从证书中提取BL2镜像的公钥/哈希,最后检查镜像内容是否匹配。如果SoC内部有OTP(一次性可编程存储),ROTPK的哈希通常会烧录在OTP中,BL1从OTP读取ROTPK的哈希来固定信任根。这样即使Flash中的固件被完全替换,由于攻击者没有私钥,也绕不过签名校验。

我在实际审计中特别关注的一个点是“哈希是否被正确比较”。有些实现里用了memcmp比较哈希,但在某些编译器优化级别下,时间侧信道会泄露比较结果,Arm官方代码在密码学比较时会用常量时间比较函数,比如crypto_hash_compare,大家在二次开发时要留意不要“优化”掉这类防护。

BL2加载完BL31后,会把一组平台参数通过bl31_params结构体传递给BL31。这些参数包括内存布局、Warm boot入口、BL32和BL33入口地址等。审计时要确认:传递的参数是否经过校验、是否可能被普通世界篡改。虽然BL2运行在安全世界,但BL2加载的镜像来源如果是外部存储,仍然需要防范存储被物理替换的风险。所以工程上BL2对BL31/BL32/BL33镜像的签名校验必不可少,不能因为BL31自己也有校验就省略前置校验。

可信启动链不只是“加个签名就完事”。ROTPK的管理、证书链的更新机制、吊销列表的处理,这三件事才是安全固件工程里的硬骨头。很多商用方案在这三个地方偷工减料,导致整个可信链形同虚设。

2.2 运行时安全服务的异常处理与上下文管理

BL31一旦驻留内存,就进入了常驻服务模式。它需要处理三类事件:SMC请求、安全世界中断、普通世界中断。异常处理的入口在bl31_entrypoint.S中,通过vector_table分发到不同异常向量。审计这里的重点是“上下文切换是否完备”。

Armv8-A的上下文切换需要保存EL3系统寄存器、SPSR、ELR,以及必要时的浮点寄存器。ATF封装了cpu_context_t结构体,cm_el1_sysregs_context_save负责保存EL1系统寄存器,而cm_el3_sysregs_context_save负责保存安全世界特定的EL3寄存器。如果代码在切换过程中漏掉某个寄存器,就可能出现“从普通世界进入安全世界后,发现DACR或TCR值完全不对”的诡异问题,这类bug非常难排查。

另一个审计重点是SMC服务的安全策略。ATF把SMC调用按function id分成多个服务类型,比如ARM_SMC_SVC_ARMARM_SMC_SVC_PSCI等。每个服务可以设置调用权限,例如某些服务只允许在Secure世界调用,而PSCI核心功能允许Non-Secure世界调用。审计时,需要逐条梳理每个SMC handler是否有必要的参数范围检查。典型漏洞就是“handler收到越界参数后,直接把它当做内存地址使用”,导致EL3内存被非安全世界任意读写。ATF源码中大量使用assert和返回值检查来缓解这类问题,但平台移植代码往往是漏洞高发区,因为很多检查会被裁剪掉以“节省空间”。

2.3 安全审计的实操切入点

做ATF安全审计,我习惯按以下步骤推进:

  1. 先看plat/平台目录下的plat_setup.cplat_security.c,确认内存访问权限和TrustZone地址空间控制器(TZASC)配置是否存在漏洞。TZASC是硬件层面对内存区域的访问控制,配置不当等于把安全世界内存敞开给普通世界,审计时必须对照内存映射表逐项核实。
  2. 再看drivers/auth/下的认证实现,重点确认ROTPK存储位置、证书解析逻辑、哈希比较方式。
  3. 然后看services/下的runtime服务实现,重点检查smc_handler的参数校验。
  4. 最后用工具做静态分析和动态插桩。静态上可以用scan-buildcppcheck扫平台代码,动态上可以在QEMU或FVP上跑起来,用GDB在异常向量处打断点,观察SMC调用链上的寄存器状态。

这个过程听起来简单,但真正做下来,需要把README、代码注释、Arm参考资料交叉着看。我遇到很多次“看代码觉得没问题,但对照Arm TRM后发现寄存器配置语义搞错”的情况,这类问题只能靠细心和对硬件的敬畏心去避免。

3. 平台移植落地:把ATF跑到一块新板卡上的完整路径

3.1 移植前必须搞清楚的平台信息

ATF移植不是把代码拷过来改个宏就能跑,它要求你对你所在平台有非常精确的认知。移植前,至少要整理出以下信息:

  • 处理器的核心型号和内核版本(比如Cortex-A72、Cortex-A53还是Ampere Altra),以及内核是否支持FEAT_GICv3、FEAT_SVE等扩展特性。
  • 内存映射:片上SRAM有哪些地址段、DDR起始地址和大小、外设寄存器的物理地址范围。
  • 中断控制器:GIC版本是GICv2还是GICv3,布局是什么样的(distributor、redistributor、CPU interface地址)。
  • 串口/调试通道:早期调试用的是哪个UART,时钟频率和基地址是多少。
  • 启动介质:BL2是从eMMC、SPI NOR还是SD卡加载BL31/BL32/BL33,存储驱动的实现要在BL2阶段搞定。

这些信息最好以表格的形式沉淀成平台文档。我见过很多移植团队上来就改代码,结果发现DDR参数和串口地址配错了,又回头查原理图,来回折腾很浪费时间。先把平台信息表整理清楚,后面所有工作都是在往这张表里填实现。

3.2 最小平台框架落地:从零添加一块开发板的目录结构

ATF官方代码已经提供了很多参考平台,比如QEMU、FVP、Juno、RPi3等。找一个结构最接近你的板子作模板,是效率最高的路径。以下以新增一个myboard平台为例,说明目录和关键文件的最小集合。

plat/myboard/ ├── platform.mk # 编译选项、镜像大小、Bl31路径等 ├── plat_def.h # 平台宏定义、内存映射大小 ├── plat_common.c # 通用启动逻辑 ├── plat_sip_svc.c # 自定义SIP服务(可选) ├── plat_topology.c # 核心拓扑描述 ├── myboard_common.c # 串口、定时器、GPIO等公共外设 ├── myboard_bl31_setup.c # BL31平台设置 ├── myboard_bl2_setup.c # BL2平台设置 ├── myboard_bl1_setup.c # BL1平台设置 ├── myboard_pm.c # 电源管理回调,PSCI实现挂钩 ├── myboard_security.c # TZASC/TrustZone配置 ├── drivers/ # 平台相关的存储/串口驱动 ├── include/ │ ├── platform_def.h # 核心地址宏定义 │ ├── myboard_def.h # 平台私有定义 │ └── plat_macros.S # 汇编宏,比如平台级别的异常向量处理 └── ...

platform.mk中,关键的变量包括BL31_SOURCESBL2_SOURCESBL1_SOURCESPLAT_INCLUDESPLAT_BL_COMMON_SOURCES。ATF的Makefile体系通过platform.mk把当前平台的源文件组织起来,最终链接出bl1.elfbl2.elfbl31.elf三个镜像。

有一个容易忽略的细节:platform_def.hplat_def.h的差别。前者通常是公共平台定义,会被上层include,后者是平台模块内部私有定义。如果不小心把两者混用,会出现宏定义冲突或者看不到定义的问题。这个问题在大型平台(比如上百个外设的SoC)上特别明显。

3.3 移植期间的关键实现细节

BL1阶段的核心工作是CPU初始化、DDR初始化、加载BL2。如果CPU的L2 cache和MMU已经在BL1阶段打开,要特别注意bl1_early_platform_setupbl1_platform_setup的实施顺序。我在实际项目里曾遇到BL1阶段MMU开启过早导致BL2镜像拷贝地址访问fault的情况,后来参照官方QEMU实现,把MMU和cache的初始化推迟到EL3的el3_entrypoint_common流程中完成,问题就消失了。所以如果平台没有特殊需求,BL1阶段就保持在“简单裸机状态”,能不用MMU就不用。

BL2阶段的重头戏是DDR初始化和镜像加载。很多SoC的DDR训练代码是厂商以二进制库形式提供的,在ATF里通过bl2_plat_handle_pre_image_loadbl2_plat_handle_post_image_load钩子挂入DDR初始化逻辑。加载BL31时,要注意ATF约定的加载地址不能和BL2自身运行时使用的内存重叠,这个地址在platform_def.h里通过宏定义,比如BL31_BASE

BL31阶段是最复杂的。启动后要先完成EL3的异常向量表设置,然后初始化GIC,注册PSCI服务,最后切换到Normal世界跳转到BL33。PSCI服务涉及CPU hotplug、suspend/resume、system off等操作,每个操作都和平台电源管理硬件紧密相关。一个常用做法是先实现最基础的PSCI_VERSIONCPU_ONSYSTEM_OFF三个功能,保证系统能启动、能关CPU、能关机,再来逐步补齐深度睡眠和唤醒路径。

移植最怕贪多求全。第一批代码只做“能启动到BL33”这一件事,所有安全特性和高级电源管理都有机会在后续迭代加上。一条能打印出串口日志的启动链路,比一千行还没验证的电源管理代码有价值得多。

3.4 构建流程和编译参数

ATF构建通常使用make,并指定CROSS_COMPILE为aarch64交叉编译工具链的前缀。一个常见的构建命令是:

make CROSS_COMPILE=aarch64-none-elf- PLAT=myboard DEBUG=1 V=1 bl31

如果平台依赖一些FDT或者特定BL33镜像地址,还需要在make命令中传入PRELOADED_BL33_BASE等参数。DEBUG=1会开启日志输出和断言,移植阶段强烈建议打开;正式发布时再关掉以减小镜像体积并去掉调试信息。

ATF的构建产物主要位于build/myboard/debug/下。bl1、bl2、bl31各自的elf、bin和map文件都在这里。map文件非常有用,排查链接地址冲突、确认代码段是否超出SRAM范围时,直接看map文件比猜地址高效得多。

如果需要把BL2和BL31打包成一个fiptool镜像,可以借助ATF自带的fiptool工具。命令类似:

fiptool create --tb-fw build/myboard/debug/bl2.bin --soc-fw build/myboard/debug/bl31.bin --nt-fw u-boot.bin fip.bin

这个fip.bin可以被BL1从启动介质中读取,然后按头部信息分发到各镜像的加载地址。理解fip的格式(每个镜像有一个ToC表项)对排查“BL31启动不起来”非常有帮助。

4. 常见问题与排查技巧实录

4.1 编译错链路对不齐

ATF对工具链版本和链接脚本比较敏感。最常见的问题是aarch64工具链版本过旧,导致汇编器无法识别某些指令,比如stlldar这些Armv8.1原子指令。另一个常见麻烦是BL31_BASE地址定义和链接脚本不一致,链接时报告“region exception vector overflow”。

对策是先确认工具链版本,Arm官方推荐使用GCC 10以上的aarch64裸机工具链。再看链接脚本platform.ld.S中的内存区间定义是否覆盖了BL31使用的所有段。如果需要调整BL31基地址,必须同步修改platform_def.h和链接脚本,否则大概率启动即挂。

4.2 上电后无任何打印

这是移植最让人崩溃的时刻。ATF里最简单的调试手段就是串口打印,如果连串口都没有输出,基本可以确定问题出在BL1阶段的时钟、引脚复用、UART基地址或波特率配置上。一个实用的排查思路:

  1. 用示波器或逻辑分析仪确认UART TX引脚是否有电平翻转。
  2. 确认UART时钟源是否已经使能,部分SoC的UART时钟默认关闭,需要先配置CRG寄存器。
  3. 确认ATF源码中plat_crash_console_init使用的UART基地址是否和你的板卡一致。
  4. 如果可能,把串口初始化代码提前到BL1最早阶段,用最短路径验证硬件通路。

在实际项目中,我曾因为“面包板上的TX/RX接反”这个低级错误折腾了一整天。所以先检查硬件,再怀疑软件。

4.3 BL31跑到一半卡死或反复重启

这类问题通常和DDR初始化、GIC配置、MMU映射有关。

如果是运行到BL31早期就重启,优先怀疑异常向量表没有正确设置,或者某个外部中断在没有GIC配置时触发,导致vector_table跳到了错误地址。可以打开CRASH_REPORTING宏,让ATF在死机时打印PC和ELR等关键寄存器信息,迅速缩小范围。

如果是在BL31进入Normal世界时卡住,重点检查传递给BL33的入口地址和参数。ATF通过bl31_params传入BL33_ENTRY_POINT,如果这个地址填错或对应内存中的代码并不是真实可执行的u-boot镜像,CPU就会“跑去执行随机数据”。此时用GDB attach到FVP或QEMU环境,检查elr_el3是否为预期地址,基本能定位问题。

4.4 Trusted Boot开启后无法启动

开启trusted boot后,BL2会先验证BL31/BL32/BL33的证书和签名。常见错误是证书链没有生成或ROTPK哈希不匹配。用调试输出定位到具体是哪个镜像验证失败,然后依次检查:

  • 镜像是否使用与平台ROTPK匹配的私钥签名。
  • 证书中填写的镜像哈希是否为当前实际镜像的哈希。
  • BL1中插入的ROTPK哈希是否与烧录进OTP的值一致。

我建议平台开发者在开发阶段把TRUSTED_BOARD_BOOT可以设为0,先跑通启动流程,再逐步加入签名验证。否则一旦签名链路有问题,很难判断是启动流程本身的问题还是安全认证的问题。

以下是移植和排查过程中的速查表,供参考:

现象可能原因排查方向
无任何输出UART配置错、时钟未使能、代码未跑到入口查串口基地址、CRG时钟、引脚复用
BL1能打印,BL2加载失败存储介质驱动问题、地址超范围查eMMC/QSPI驱动、确认FIP镜像地址
BL31_print后挂起GIC未初始化、MMU映射缺失、上下文保存不完备检查GIC地址和初始化顺序,打开CRASH_REPORTING
PSCI CPU_ON无效电源域没有配置、CPU电源控制器寄存器不对对照SoC TRM确认CPU power domain操作序列
Trusted Boot校验失败ROTPK不一致、证书没用对私钥签名、镜像哈希不匹配重新生成证书链,核对ROTPK哈希

5. 移植落地之外:ATF安全固件工程的长期维护视角

5.1 与上游社区保持同步

ATF的开源社区迭代速度很快,Arm会定期发布安全公告和bug修复。如果平台代码长期停留在某个内部版本,就可能错过关键的安全补丁。平台移植团队最好建立一套“定期合并上游、持续适配本地补丁”的机制。一个可行的做法是把平台特定的改动尽量收拢在plat/目录和少数几个include文件内,做到每季度执行一次rebase或merge操作。这样即使有代码冲突,也容易被定位和解决。

5.2 设计评审与安全测试

我自己评审ATF相关改动时,必看的几个点包括:新增SMC服务是否有权限校验、内存映射是否给普通世界留了可写窗口、TZASC配置是否覆盖了所有安全内存、trusted boot证书链的更新流程是否闭环。安全测试也不是只在实验室里做,建议在CI流程中加入FVP或硬件在环的启动测试,每次提交后自动跑一遍BL1→BL31→BL33的完整启动和几个核心SMC调用测试,把回归风险降到最低。

5.3 从启动到可信根的整体视角

最后还是想强调那个理念:ATF不是孤立的bootloader,它是从“上电即执行的可信根”到“运行时的安全服务框架”的完整体系。评估ATF移植是否成功,不能只看能不能启动U-Boot,还要看:有没有把安全启动链路打通、运行时的PSCI和SMC服务是否稳定、安全世界和普通世界的中断隔离是否严密、异常和上下文切换是否可靠。一个真正合格的ATF落地,是让上层OS感觉不到它的存在,同时又必须时刻在关键路径上守护系统安全。

我在实际移植中体会最深的一点是:ATF的源码质量在开源固件里属于非常高的水平,阅读它本身就能学到大量Arm体系结构的设计思想。哪怕你的项目只是用ATF做快速启动,也建议把BL31的异常处理、PSCI框架和认证框架都粗读一遍。这些代码里浓缩的工程智慧,比很多纸上谈兵的教材有价值得多。

最后再分享一个建议:不要只在真机上调ATF。先把FVP或QEMU环境搭起来,很多启动链路问题在模拟器里更容易暴露和定位。当你在模拟器里把整个流程跑得滚瓜烂熟之后,真机上踩坑的概率会小非常多。这套“模拟器先行、真机验证”的方法,是我在多个ATF平台移植项目里反复验证过的有效路径。

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

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

立即咨询