news 2026/9/8 12:37:28

Arm Trusted Firmware(ATF)架构解析与平台移植实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Arm Trusted Firmware(ATF)架构解析与平台移植实战

做嵌入式底层的人,迟早得跟Arm Trusted Firmware(ATF)打交道。不管你是在调OP-TEE、看U-Boot从EL2跳转,还是排查Linux内核在EL1启动前的异常,ATF始终是绕不开的那一环。我最早接触它的时候还叫ARM Trusted Firmware,这两年官方改名TF-A(Trusted Firmware-A),但社区里大家还是习惯喊ATF。这篇文章我想从源码角度把ATF的架构讲透,再把平台移植的完整路径走一遍。先说结论:ATF不是一个传统意义的bootloader,它是负责在EL3建立安全世界执行环境的可信固件,是整个TrustZone生态的基石。适合正在啃ATF源码、准备做新平台适配,或者想搞明白安全启动链路的人看。

1. ATF全景认知:安全固件到底在启动链路里扮演什么角色

1.1 从一次上电说起:ATF存在的根本理由

芯片上电之后,CPU从复位向量开始执行,这一步的代码通常固化在ROM里。接下来要面对一个很现实的问题:引导链上每一级软件的合法性由谁来保证?如果随便一段代码都能跳到EL3去执行,那TrustZone内存隔离做得再好也没有意义。ATF之所以能成为Arm平台安全启动的事实标准,核心原因就是它把“信任根”和“信任链传递”的机制落到了代码层。换句话说,从芯片出厂那一刻起,验证下一阶段镜像的责任就被交给了一段不可篡改的ROM代码,这段代码在Arm方案里就是BL1。

我实际看过的很多平台,BL1跑完后会加载BL2,BL2负责验证并加载BL31、BL32(如果有TEE的话)和BL33。这套流程对应的是Arm定义的Trusted Board Boot(TBB)规范。你可能要问,为什么不能一个阶段全干完?原因很实际:ROM容量有限,安全性和灵活性没法同时满足。BL1只做最小初始化,保证BL2镜像被安全加载;BL2则承担了证书解析和镜像验证的工作。这种分级设计在工程上的好处是,ROM代码几乎不用改动,后续安全策略升级只动BL2或BL31就行。

作为开发者,我们真正需要关心的通常从BL2开始,因为很多平台并不会开放BL1的ROM源码。如果你用的是树莓派、瑞芯微或者全志这类商用SoC,BL1要么在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指令陷入EL3,BL31中的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、pmf(Performance 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这类真实平台适合看厂商的实现风格。整个源码的编译入口在顶层Makefile,make PLAT=xxx即可,但这只是表象,内部依赖关系比想象中复杂得多。

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默认支持TBBR(Trusted Board Boot Requirements)规范。它的思路是:BL1在ROM中预置了root-of-trust公钥,BL2加载的每个镜像都附带一张X.509证书,证书里写明镜像的哈希值和签名。BL2用公钥验签成功后才会加载执行。

具体的证书生成和验签流程在drivers/auth/目录。代码里用到了mbedtls库来做加密运算。不同的平台可以选择不同的证书格式,Arm官方提供了cert_create工具来生成证书链。你需要定义自己的CoT(Chain of Trust)结构,告诉工具哪些证书是必须签的,哪个密钥是顶级信任根。

做工程审计时,我建议重点看两个文件:plat/<platform>/include/platform_def.h里的TBBR配置开关,以及plat/<platform>/plat_tbb.c(或类似命名)里的证书描述表。安全强度取决于私钥保护措施:如果生产环境的签名私钥被导出到不安全的地方,那TBBR就形同虚设。很多做产品的人以为烧了TBBR就万事大吉,实际漏洞往往出在密钥管理流程上。

2.4 安全审计重点关注哪些文件

审计一份ATF源码是否健壮,我个人有固定的检查路线。第一站是内存映射相关代码,尤其是bl31_plat_get_next_bl_paramsbl31_plat_runtime_setup。如果内存区域的属性配置过宽,比如把安全DRAM映射成可写非安全,那就等于给攻击者开了后门。第二站是SMC处理函数,检查有没有对参数做充分的边界校验。之前CVE列表里好几条ATF漏洞都是SMC参数检查不严导致的越权访问。第三站是TZASC(TrustZone Address Space Controller)的配置,这颗IP专门控制哪些地址区域允许安全访问、哪些允许非安全访问。

审计时有个实用技巧:开启LOG_LEVEL=50编译后,BL31会输出大量调试信息,可以清晰看到每个内存区域的映射属性和SMC分发的路径。但这只能作为辅助手段,真正的安全性需要逐行看代码逻辑。

3. 平台移植落地:从零搭建一个新平台的完整实操

3.1 移植前必须想清楚的四个问题

很多人拿到新SoC就想立刻把ATF跑起来,结果卡在各种隐藏问题上。动手之前,一定要先回答这几个问题:

  1. 你的SoC主核是Armv8-A吗?Armv7的芯片走的是另一套逻辑,ATF主要针对Armv8及以上的64位核心。
  2. BL1能不能复用?如果芯片ROM已经提供了固化的BL1,你可能只需要做BL2和BL31。
  3. BL33打算用U-Boot还是UEFI?这决定了BL31跳转时传递给BL33的参数结构。
  4. 安全世界有没有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_SIZEPLAT_VIRT_ADDR_SPACE_SIZE:物理和虚拟地址空间大小,决定MMU页表覆盖范围。
  • BL31_BASEBL31_LIMIT:BL31在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_MAJORARM_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_COMPILE=aarch64-none-linux-gnu- PLAT=<your_platform> DEBUG=1 \ LOG_LEVEL=50 V=1 BL33=<path_to_bl33>/u-boot.bin

DEBUG=1会关闭优化并生成调试符号,推荐开发阶段使用。LOG_LEVEL=50把日志开到最详细级别,很多问题靠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代码,按顺序检查:

  1. 串口引脚复用了没有?很多SoC的UART引脚还兼任GPIO功能,需要在pinctrl里配好。
  2. UART时钟频率和分频系数正确吗?ATF里通常用固定的波特率计算分频值,如果时钟树没初始化或时钟频率和platform_def.h里的定义不一致,输出就会是乱码或直接没输出。
  3. 串口驱动注册了吗?BL31里有没有调用console初始化?如果连console_16550_register都没调用,自然不会有输出。

还有一个大部分人不知道的细节:加LOG_LEVEL=50编译后,log会前移到更早的阶段。这样你在BL31进入主流程之前就能看到执行路径了。但前提是early console已经在BL31入口附近初始化好。

4.2 编译阶段的隐性坑

提到编译问题,很多人第一反应是缺头文件、缺工具链。但ATF还有一个很隐蔽的坑:它如果要生成TBBR证书,编译时会自动调cert_create工具,这个工具依赖的OpenSSL库版本会对编译结果产生影响。我在Ubuntu 22.04上编译时,遇到过OpenSSL 3.0的API弃用警告导致编译中断的情况,解决办法是升级ATF版本或改用相对老一些的发行版编译环境。

另一个坑是某些平台在Makefile里启用了ENABLE_AMUENABLE_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放到期望的地址,需要检查加载脚本和链接脚本是否一致,可以用nmobjdump查看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.cpsci_cpu_on.c,接着对照参考平台把每个回调函数走一遍,最后再动自己的平台代码。这个过程急不得,但走完一遍之后,再去看OP-TEE、UEFI Secure Boot,你会发现很多概念是相通的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 12:37:22

LabVIEW RT目标上DLL与INI配置文件的部署与调用指南

把自定义 DLL 和 INI 配置文件部署到 LabVIEW RT&#xff08;实时&#xff09;目标&#xff0c;这个需求我遇到太多次了。很多工程师在 Windows 端把上位机跑通了&#xff0c;程序写得贼顺&#xff0c;一到要往 CompactRIO、PXI 或者工控机实时目标上部署时&#xff0c;就开始踩…

作者头像 李华
网站建设 2026/9/8 12:36:59

AI代理上下文工程:从生命周期到治理的实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:36:41

轨道交通线网指挥中心:核心引擎、建设难点与工程实践

凌晨一点多&#xff0c;最后几班列车还在隧道里跑&#xff0c;线网指挥中心的大屏一片深蓝。值班长盯着晚点指标&#xff0c;手边的即时通信窗口里&#xff0c;几条线路的行车调度员几乎同时发来同样的问题&#xff1a;末班车延误能否顺延换乘等待&#xff1f;这一刻&#xff0…

作者头像 李华
网站建设 2026/9/8 12:36:05

多AGV调度系统架构设计与路径规划避碰策略实战

简介&#xff1a;多AGV调度系统软件是一套基于JAVA的自动化物流解决方案&#xff0c;适用于智能仓储与智能制造场景&#xff0c;面向需要研究多机器人协同调度、路径规划与任务分配的开发者、方案工程师及高校师生。软件包内含1260个文件&#xff0c;主要包括923个java源码、16…

作者头像 李华
网站建设 2026/9/8 12:35:31

Java对接NTP服务器实现高精度时间同步的完整指南

1. 项目概述与NTP接入的整体思路1.1 为什么要独立对接NTP服务器做Java开发时间久了&#xff0c;你会发现一个特别容易被忽略但又特别要命的问题&#xff1a;服务器时间不准。我最早是在一次日志排查中踩到坑的&#xff0c;两台应用服务器日志时间差了将近40秒&#xff0c;联调接…

作者头像 李华
网站建设 2026/9/8 12:34:24

Mediapipe Holistic Tracking:人体姿态、手部与面部关键点统一追踪实践

简介&#xff1a;面向人体姿态估计与手势识别学习者的 Mediapipe 整体跟踪工具&#xff0c;基于谷歌开源库 Mediapipe 的 Python API 实现&#xff0c;可对视频中的人脸、手部与人体姿态关键点进行同步检测与跟踪&#xff0c;广泛应用于动作分析、健身辅助、虚拟数字人等场景。…

作者头像 李华