Arm Trusted Firmware(ATF)架构解析与平台移植实战 做嵌入式底层的人迟早得跟Arm Trusted FirmwareATF打交道。不管你是在调OP-TEE、看U-Boot从EL2跳转还是排查Linux内核在EL1启动前的异常ATF始终是绕不开的那一环。我最早接触它的时候还叫ARM Trusted Firmware这两年官方改名TF-ATrusted Firmware-A但社区里大家还是习惯喊ATF。这篇文章我想从源码角度把ATF的架构讲透再把平台移植的完整路径走一遍。先说结论ATF不是一个传统意义的bootloader它是负责在EL3建立安全世界执行环境的可信固件是整个TrustZone生态的基石。适合正在啃ATF源码、准备做新平台适配或者想搞明白安全启动链路的人看。1. ATF全景认知安全固件到底在启动链路里扮演什么角色1.1 从一次上电说起ATF存在的根本理由芯片上电之后CPU从复位向量开始执行这一步的代码通常固化在ROM里。接下来要面对一个很现实的问题引导链上每一级软件的合法性由谁来保证如果随便一段代码都能跳到EL3去执行那TrustZone内存隔离做得再好也没有意义。ATF之所以能成为Arm平台安全启动的事实标准核心原因就是它把“信任根”和“信任链传递”的机制落到了代码层。换句话说从芯片出厂那一刻起验证下一阶段镜像的责任就被交给了一段不可篡改的ROM代码这段代码在Arm方案里就是BL1。我实际看过的很多平台BL1跑完后会加载BL2BL2负责验证并加载BL31、BL32如果有TEE的话和BL33。这套流程对应的是Arm定义的Trusted Board BootTBB规范。你可能要问为什么不能一个阶段全干完原因很实际ROM容量有限安全性和灵活性没法同时满足。BL1只做最小初始化保证BL2镜像被安全加载BL2则承担了证书解析和镜像验证的工作。这种分级设计在工程上的好处是ROM代码几乎不用改动后续安全策略升级只动BL2或BL31就行。作为开发者我们真正需要关心的通常从BL2开始因为很多平台并不会开放BL1的ROM源码。如果你用的是树莓派、瑞芯微或者全志这类商用SoCBL1要么在maskrom里要么由芯片厂商的专有代码直接接管。所以实际移植工作的重头戏集中在BL2和BL31。1.2 四个BL阶段与Arm异常级别的映射关系理解ATF必须先理解Armv8-A的异常级别。简单说EL0到EL3是四个特权等级EL0权限最低EL3最高。ATF的每个BL阶段其实都对应着一段特定的执行环境BL1运行在EL3负责从复位状态接管系统最小化初始化后加载BL2。BL2运行在EL2也可以配置为EL1负责安全加载和验证后续镜像。BL31运行在EL3是常驻的安全运行时固件。系统正常启动后BL1和BL2的生命周期基本结束BL31一直驻留处理来自非安全世界的SMC请求。BL32运行在EL1或EL0一般是可信操作系统如OP-TEE属于可选项。BL33运行在EL2或EL1就是最终的正常世界引导程序常见的是U-Boot或UEFI。我把这几个阶段理解成一条流水线BL1像工厂门禁只验证第一个合格证BL2像质检员把后续货物全部过一遍BL31则是常驻的保安控制室所有跨界事项都得通过它。日常调试中你看到串口输出Trusted Firmware相关的log时通常就是BL2或BL31在工作。值得注意的是BL31不是只在启动瞬间工作它常驻EL3为整个操作系统运行期间提供安全服务。这就是为什么它被称为runtime firmware。CPU的电源管理、系统挂起恢复、SMC中断路由这些指令最终都要落到BL31里的PSCI实现上。1.3 为什么说ATF是TrustZone生态的基石TrustZone的概念是把硬件资源分成安全世界和非安全世界两个隔离域。但光有硬件隔离还不够软件上必须有一条明确的路由规则谁可以进入安全世界、安全世界如何响应非安全世界的请求。ATF就是这条规则的执行者。具体到代码层面非安全世界的内核或hypervisor通过SMC指令陷入EL3BL31中的runtime service框架收到SMC后根据服务ID分发到对应的处理函数。比如PSCI功能、OP-TEE的SMC接口本质上都是经由EL3中转或直接转交给BL32。这个机制保证了安全世界与非安全世界的每一次交互都有明确的入口和出口不会有旁路可走。我在做平台移植时最大体会是ATF的架构思路和普通MCU的bootloader完全不同。它不是在“拉起下一个镜像”而是在搭建一套“安全边界管理框架”。这也解释了为什么ATF源码里有大量平台抽象层代码——每个SoC在GIC、串口、内存控制器上的差异都必须在平台层消化掉而安全框架本身保持通用。2. 源码工程审计ATF的目录结构与核心模块逐层拆解2.1 顶层目录怎么看才能不迷路第一次打开ATF源码库很多人容易被一堆目录吓到。其实只要抓住几个关键目录就够了bl1/、bl2/、bl31/各阶段的入口和主流程代码。plat/平台移植的核心目录按厂商和开发板组织。lib/通用库包括el3_runtime、el3_common、pmfPerformance Measurement Framework等。drivers/各种外设驱动包括arm/gic、console/uart、auth等。include/头文件分platform、bl除外还有lib子目录。common/BL阶段的公共代码比如描述数据结构的bl_common.c。tools/fiptool和证书生成工具打包FIP镜像时必用。我拿到一份陌生的ATF代码第一步永远是看plat/下面有没有跟目标SoC接近的参考平台。比如qemu、fvp这类虚拟平台是入门首选rockchip、allwinner这类真实平台适合看厂商的实现风格。整个源码的编译入口在顶层Makefilemake PLATxxx即可但这只是表象内部依赖关系比想象中复杂得多。2.2 BL31的runtime服务SMC分发与PSCI实现剖析BL31是整个ATF里最值得精读的部分。它启动后在EL3完成必要的初始化然后进入主循环。这里的核心数据结构是一张runtime service描述表每个服务通过DECLARE_RT_SVC宏注册带上服务的ID范围、初始化函数、SMC处理回调。SMC分发逻辑在bl31/runtime_svc.c里当收到一条SMC请求系统会从fastcall或yielding call中取出服务ID在服务表中查找匹配项找不到就返回NOT_SUPPORTED。这个设计非常像Linux内核的驱动模型——用一张注册表把各种安全功能解耦。PSCI是ATF里最典型的runtime service。CPU启动、关闭、系统挂起这些操作都通过PSCI接口实现。实现细节上psci_setup.c负责初始化电源管理状态psci_cpu_on.c处理核心上电流程。做移植时最常改的就是这里因为每个SoC的电源控制器操作方式天差地别。我调过某颗国产SoC光是CPU_ON的寄存器序列就折腾了两周最后发现是L2 cache的flush时机不对。2.3 TBBR证书链与信任根设计安全启动这块ATF默认支持TBBRTrusted Board Boot Requirements规范。它的思路是BL1在ROM中预置了root-of-trust公钥BL2加载的每个镜像都附带一张X.509证书证书里写明镜像的哈希值和签名。BL2用公钥验签成功后才会加载执行。具体的证书生成和验签流程在drivers/auth/目录。代码里用到了mbedtls库来做加密运算。不同的平台可以选择不同的证书格式Arm官方提供了cert_create工具来生成证书链。你需要定义自己的CoTChain of Trust结构告诉工具哪些证书是必须签的哪个密钥是顶级信任根。做工程审计时我建议重点看两个文件plat/platform/include/platform_def.h里的TBBR配置开关以及plat/platform/plat_tbb.c或类似命名里的证书描述表。安全强度取决于私钥保护措施如果生产环境的签名私钥被导出到不安全的地方那TBBR就形同虚设。很多做产品的人以为烧了TBBR就万事大吉实际漏洞往往出在密钥管理流程上。2.4 安全审计重点关注哪些文件审计一份ATF源码是否健壮我个人有固定的检查路线。第一站是内存映射相关代码尤其是bl31_plat_get_next_bl_params和bl31_plat_runtime_setup。如果内存区域的属性配置过宽比如把安全DRAM映射成可写非安全那就等于给攻击者开了后门。第二站是SMC处理函数检查有没有对参数做充分的边界校验。之前CVE列表里好几条ATF漏洞都是SMC参数检查不严导致的越权访问。第三站是TZASCTrustZone Address Space Controller的配置这颗IP专门控制哪些地址区域允许安全访问、哪些允许非安全访问。审计时有个实用技巧开启LOG_LEVEL50编译后BL31会输出大量调试信息可以清晰看到每个内存区域的映射属性和SMC分发的路径。但这只能作为辅助手段真正的安全性需要逐行看代码逻辑。3. 平台移植落地从零搭建一个新平台的完整实操3.1 移植前必须想清楚的四个问题很多人拿到新SoC就想立刻把ATF跑起来结果卡在各种隐藏问题上。动手之前一定要先回答这几个问题你的SoC主核是Armv8-A吗Armv7的芯片走的是另一套逻辑ATF主要针对Armv8及以上的64位核心。BL1能不能复用如果芯片ROM已经提供了固化的BL1你可能只需要做BL2和BL31。BL33打算用U-Boot还是UEFI这决定了BL31跳转时传递给BL33的参数结构。安全世界有没有OP-TEE没有的话BL32可以完全不用编译对应在编译参数里关闭即可。如果平台跟某个已有参考板非常相似比如同SoC系列、同一套GIC版本最快的方式是把参考板的平台目录整份拷贝过来修改厂商名、板名和关键配置。很多开源SoC的BSP就是这么做的我见过瑞芯微的多个平台从rk3399移植到rk3588改动量其实很小主要涉及DDR初始化参数和外设基地址。3.2 平台目录搭建与platform_def.h必配项解析ATF的平台代码组织方式是plat/vendor/platform。最简单的起步是从qemu或fvp平台复制一份保留通用的驱动和库代码替换平台专属部分。最关键的文件是include/platform_def.h这里定义了几乎所有的硬件参数。我挑几个必配项说明PLAT_PRIMARY_CPU主核心ID冷启动时其它核心会等待主核心负责初始化。PLAT_MAX_CPUS核心总数影响PSCI的状态数组大小。PLAT_PHY_ADDR_SPACE_SIZE和PLAT_VIRT_ADDR_SPACE_SIZE物理和虚拟地址空间大小决定MMU页表覆盖范围。BL31_BASE、BL31_LIMITBL31在DRAM或SRAM中的加载区域。MHU_BASE如果涉及到与SCP通信、GIC_BASE外设基地址。PLAT_ARM_TRUSTED_SRAM_BASE安全SRAM的基地址和大小。这些参数定义错一个轻则启动卡死重则内存越界导致安全灾难。我在调一个新的RISC-V类定制SoC时曾经把BL31_BASE设到了非安全区域结果BL31被正常世界的U-Boot覆盖整机随机死机。后来排查了很久发现是链接脚本和platform_def.h的地址不一致。另一个容易忽略的是ARM_ARCH_MAJOR和ARM_ARCH_MINOR宏。它们告诉编译器当前SoC支持的最低Arm架构版本。设置过高会导致CPU运行不支持的指令设置过低则可能失去某些优化机会。3.3 汇编启动文件与内存映射编写的关键细节ATF每个BL阶段都有自己的启动文件通常在plat/platform/aarch64/platform_common.c或.S文件里。BL31的启动流程大致是设置异常向量表配置系统寄存器初始化内存和MMU然后进入主流程bl31_main。这里有三个关键细节踩坑后印象特别深第一异常向量表必须放在BL31镜像里合适的位置。ATF用CTX_SAVE机制保存CPU上下文如果向量表地址没按64字节对齐异常处理时会发生不可预期的跳转。第二MMU初始化不是直接开缓存就完事。ATF通常先建立页表把所有需要访问的区域映射好再开MMU。映射属性里的XN不可执行、非安全位、访问权限都要仔细斟酌。我习惯用mmap_add_region逐段添加映射配完后再用xlat_tables_init初始化。第三BL31启动时会把CPU异常级别从EL3降到适当的EL并跳转到BL33。这个过程在el3_exit里实现。它会恢复BL33的上下文设置SCR_EL3的相应位让BL33以预期的安全状态运行。如果这一步寄存器状态没准备好跳过去后BL33会直接异常。还有一个容易踩的坑是串口初始化时机。BL31的log在bl31_early_platform_setup之前是看不见的很多人以为代码没走其实是串口还没配置好。一般平台会在early setup里调用console_16550_register之类的接口把串口驱动装上。3.4 编译、FIP打包与烧写调试全流程ATF编译不像普通Linux内核那样一个make就能完事。你需要先设置交叉编译器环境变量然后执行git clone https://github.com/ARM-software/arm-trusted-firmware.git cd arm-trusted-firmware make CROSS_COMPILEaarch64-none-linux-gnu- PLATyour_platform DEBUG1 \ LOG_LEVEL50 V1 BL33path_to_bl33/u-boot.binDEBUG1会关闭优化并生成调试符号推荐开发阶段使用。LOG_LEVEL50把日志开到最详细级别很多问题靠log就能定位。编译产物中build/platform/debug/bl31.bin是我们最需要的镜像之一。如果有BL32的话bl32.bin也会生成。但直接用bin不如生成完整的FIP镜像方便。FIP是ATF自己的固件打包格式里面可以装载BL2、BL31、BL32、BL33以及证书等。用fiptool生成FIP的命令大概是这样tools/fiptool/fiptool create --tb-fw build/platform/debug/bl2.bin \ --soc-fw build/platform/debug/bl31.bin \ --nt-fw path_to_bl33/u-boot.bin \ --tos-fw build/platform/debug/bl32.bin \ fip.bin如果没有BL32后面的--tos-fw参数可以直接省略。烧写环节具体方式取决于SoC支持哪些启动介质——有的走SD卡有的走eMMC有的走Flash。但不管哪种方式基本的做法是在指定偏移量处写BL1和FIP或者直接把FIP合入一个完整的启动镜像。调试阶段我强烈建议大家准备一套好的调试工具组合串口log是必备整机仿真器或JTAG在早期没什么可看的。最痛苦的往往不是代码逻辑而是硬件验证环境的差异——我自己遇到过多次在qemu上跑得好好的代码到了真板上就起不来最后定位都是时钟或DDR初始化相关。4. 常见问题与排查技巧实录4.1 串口无输出问题平台移植初期最典型的问题就是串口一个字符都不打印。出现这个现象先别急着怀疑BL31代码按顺序检查串口引脚复用了没有很多SoC的UART引脚还兼任GPIO功能需要在pinctrl里配好。UART时钟频率和分频系数正确吗ATF里通常用固定的波特率计算分频值如果时钟树没初始化或时钟频率和platform_def.h里的定义不一致输出就会是乱码或直接没输出。串口驱动注册了吗BL31里有没有调用console初始化如果连console_16550_register都没调用自然不会有输出。还有一个大部分人不知道的细节加LOG_LEVEL50编译后log会前移到更早的阶段。这样你在BL31进入主流程之前就能看到执行路径了。但前提是early console已经在BL31入口附近初始化好。4.2 编译阶段的隐性坑提到编译问题很多人第一反应是缺头文件、缺工具链。但ATF还有一个很隐蔽的坑它如果要生成TBBR证书编译时会自动调cert_create工具这个工具依赖的OpenSSL库版本会对编译结果产生影响。我在Ubuntu 22.04上编译时遇到过OpenSSL 3.0的API弃用警告导致编译中断的情况解决办法是升级ATF版本或改用相对老一些的发行版编译环境。另一个坑是某些平台在Makefile里启用了ENABLE_AMU、ENABLE_SPE_FOR_LOWER_ELS等功能开关这些开关可能依赖CPU的特定实现。如果SoC不支持这些扩展就必须在platform_def.h里把它们关闭。工具链选型上优先用Arm官方推荐的aarch64-none-linux-gnu-或老版本里的aarch64-linux-gnu-。不要轻易用太新或太老的GCC版本——ATF对编译器的优化行为很敏感换一个版本连接脚本中布局就可能发生变化严重时直接导致BL31镜像超限或运行异常。网上搜索到的arm compiler 5.06这类旧编译器建议不要用于ATF 2.x这种新代码。4.3 运行阶段异常定位方法论如果代码编过了镜像也烧进去了但运行到一半就死掉这通常是最费时间的阶段。我的定位套路是第一看log停在哪一行。这是最重要的线索。比如log停在bl31_plat_arch_setup说明MMU或内存配置有问题停在psci_setup说明电源管理相关配置出错。第二确认BL31镜像是否正确加载到指定地址。有些SoC的Loader并不会把BL31放到期望的地址需要检查加载脚本和链接脚本是否一致可以用nm或objdump查看bl31.elf的符号地址来反推。第三如果log一只重复打印或者卡在一个循环里多半是某个等待超时机制没配好。比如BL31等BL32启动而BL32根本没被加载系统就会一直等下去。此时要看BL32镜像是否存在于FIP包里以及它的加载地址是否可用。第四尝试关闭MMU缓存再跑一遍看问题是否和缓存一致性有关。这个方法虽然粗暴但能快速缩小排查范围。ATF的MMU实现相比Linux内核简单得多问题通常出在页表映射属性上。4.4 移植经验小结一次真实踩坑记录最后分享一次我调ATF的真实经历。当时在做一块基于某Armv8处理器的评估板移植板子SDK自带的U-Boot已经能正常启动Linux但ATF就是带不起来。最诡异的是BL31 log显示一切正常跳到BL33后U-Boot跑了几行也正常但一进内核就随机死机。我把怀疑对象锁定在BL31对内存区域的安全属性配置上。后来反复比对参考板和我的platform_def.h终于发现问题我把PLAT_PHY_ADDR_SPACE_SIZE定义得过大导致TLB的映射范围覆盖了DMA缓冲区而某些DMA缓冲区被误标成了安全内存。内核访问这块内存时EL3的TZASC直接拦下来报了错。这个错误在qemu上完全复现不了因为qemu的TZASC没有严格检查访问权限。那次之后我养成了一个习惯平台内存映射表会一个人一个人地核对哪怕参考平台已经验证过也要重新推导一遍每个区域的属性和访问权限而不是盲目照抄。平台移植不是把代码拷过来改个Makefile就完事你需要真正理解每一行配置背后的安全意图和硬件约束。另外还想多说一句遇到问题不要只盯着ATF本身。ATF只是启动链上的一环前有BL1/ROM代码后有BL33/U-Boot。很多看似在ATF里出现的问题根源可能在上一级Loader传给BL31的参数不对或者在BL33接收参数时解析出错。把这整条链路作为一个闭环来看排查效率会高很多。如果后续你还想深入建议按这个顺序继续啃先完整读一遍bl31_main.c然后是runtime_svc.c和psci_cpu_on.c接着对照参考平台把每个回调函数走一遍最后再动自己的平台代码。这个过程急不得但走完一遍之后再去看OP-TEE、UEFI Secure Boot你会发现很多概念是相通的。