ARM可信固件ATF深度解析:从架构全景到平台移植实战

📅 发布时间:2026/9/4 12:42:06
ARM可信固件ATF深度解析:从架构全景到平台移植实战
1. 为什么ATF值得你花时间啃源码做ARM平台开发这些年我越来越觉得Arm Trusted-FirmwareATF是一块绕不开的硬骨头。你可能在uboot阶段看过它、在OP-TEE启动日志里见过它、在PSCI电源管理里隐约感觉到它但真要让你说清楚它到底干了什么恐怕一时半会儿也讲不透。这篇内容我打算从架构全景、源码组织、工程审计、平台移植几条线展开把我自己在实际项目里跑到的那套理解整理出来给正在踩坑或准备入坑的朋友做个参照。先说清楚ATF到底是什么。它的官方名全称是Arm Trusted Firmware是ARM为Armv8-A/Armv9-A架构提供的一套开源安全固件参考实现运行在EL3异常级别承担着安全启动、运行时安全服务、世界切换World Switch、电源状态协调PSCI等一系列核心职责。可以这么理解在ARM世界里ATF就是普通世界与安全世界之间的一道闸门也是系统冷启动时最早执行的那部分可信代码。这套东西适合谁来学我个人觉得分三类人第一类是BSP或固件工程师要交付一个完整可启动的系统绕不开ATF的移植与集成第二类是安全方向的开发者想搞懂可信启动链、内存隔离、TEE环境之间的信任关系第三类是应用层或内核层做底层研究的人搞明白ATF也能反过来帮你理解内核里的psci、smc调用这些机制到底在做什么。我最早接触ATF纯属被项目逼的。当时需要在一颗自研SoC上把Linux跑起来原先默认的方式是从uboot直接跳转kernel压根没想过去碰安全侧的东西。直到产品提出要支持安全启动和TEE环境我才真正开始读ATF源码越读越觉得这块代码的设计相当精妙它把硬件细节、安全边界、系统初始化全都糅在了一个只有几万行代码的工程里。整个过程踩了不少坑也积累了一些自己的理解下面按我的思路一条条展开。2. ATF架构全景从BL1到BL33一条完整的可信启动链2.1 启动阶段的整体流程与角色分工如果你手头有一块基于Armv8-A的板子按下电源键之后CPU首先运行的是固化在ROM里的代码这部分通常叫BootROM或者固化引导代码它负责最基础的硬件初始化和加载第一段可信固件。在ATF的语境里这段被加载的固件就是BL1。BL1是整个信任链的起点它体积小、生命周期极短主要任务就两个一是初始化必要的基础硬件比如串口、DDR控制器、TrustZone地址空间控制器二是将BL2加载进安全内存并完成验证。BL2运行在EL1的Secure状态它做的事情更像一个“启动管理器”。BL2负责解析平台描述信息比如FIP里封装了哪些镜像、各自在什么地址、用什么方式校验然后把BL31、BL32如果有TEE的话以及BL33通常是UEFI或者uboot加载到对应位置。注意这里所有加载动作之前都会做签名校验和哈希比对保证内存里的镜像确实来自于可信来源。校验通过之后BL2完成任务把控制权交给BL31自己退出执行。BL31是ATF真正的主角也是代码量最大、逻辑最复杂的部分。它运行在EL3是系统的Secure Monitor。BL31常驻内存不随启动结束而销毁它对外提供一套基于SMCSecure Monitor Call的运行时服务包括标准的PSCI电源管理服务、SIPSilicon Provider厂商自定义服务、TOSTEE OS的调度服务等。普通世界的内核想要操作CPU的热插拔、进入深睡眠、做系统重启都必须通过SMC指令陷入EL3由BL31代为执行。这个设计从底层上保证了电源操作只能由可信固件来发起普通内核没有权限直接碰电源控制器。BL32通常是OP-TEE和BL33普通世界引导程序则分别代表安全世界和普通世界的上层软件。ATF本身不关心BL32和BL33具体跑的是什么只要它们遵循SMC约定和二进制接口规范就能在BL31的协调下完成世界切换。2.2 BL31运行时服务框架SMC调度、PSCI与TOS服务BL31的运行时逻辑核心是中断与异常分发具体到代码就是bl31_main和runtime_svc框架。大致流程是内核或TEE通过SMC指令触发EL3异常BL31捕获后解析SMC功能IDFID根据FID里编码的服务类型是PSCI标准服务、OP-TEE服务还是厂商私有服务分发到对应的服务处理函数。这个过程有点像操作系统里的系统调用表只不过这张表维护在两个世界之间。PSCI是其中最重要的一条服务线。它规定了CPU的开关机、suspend/resume、系统off/reset等电源操作的统一接口。ATF对PSCI的实现藏在services/std_svc/psci目录下涉及状态机设计、电源域拓扑、中断唤醒路径。现在回过头看这份代码对理解ARM电源管理模型非常有帮助——你知道suspend的时候CPU要走到哪一步、cache要如何维护、中断控制器要如何配置这些细节在PSCI实现里全都有答案。TOS服务则负责在EL3和TEE之间做转发。举个例子CAClient Application在普通世界调用一个TATrusted Application接口正常的调用链是这样的普通应用call内核内核走SMC陷入EL3BL31识别出这是发给TEE的请求然后把参数打包之后再通过SMC切到安全世界最终由OP-TEE在S-EL1接到请求跑到TA逻辑原路返回结果。BL31在中间纯粹起路由与隔离作用它自身不会去解释TA的具体指令但它保证了两个世界上下文的安全切换与寄存器状态的完整保存/恢复。2.3 几个容易被忽视的关键设计思想读ATF源码有几个设计思想如果不理解后面的移植和排错都很容易走偏。第一个是“权能最小化”。BL1的代码量很小而且不需要的驱动代码绝不包含因为BL1的安全性直接决定整个信任链是否可信。哪怕是BL2也只是把镜像加载和验签逻辑做好不掺多余功能。第二个是“上下文分离”。每个异常级别、每个世界都有独立的寄存器上下文保存在EL3的专属内存区域里切换时直接换上下文而不是简单跳转。这个设计保证即使普通世界被攻破也无法拿到安全世界残余的寄存器数据。第三个思想是“平台抽象”。ATF目录下有一大堆plat/子目录对应各种官方开发板和芯片平台这种平台抽象加上编译期的宏控制让同一套源码可以适配几十种硬件这也是它作为参考实现被广泛采用的原因。搞清楚这几个思想之后你会发现ATF的思路其实和微内核操作系统很像——尽量小的可信计算基、明确的接口边界、严格的数据隔离。3. 源码工程审计ATF目录结构、核心模块与编译体系3.1 源码总览与核心目录用途拿到一份ATF源码第一件事别急着读代码先把目录结构摸清楚。顶层目录包含bl1、bl2、bl31这三个分别对应各启动阶段的实现代码以及common公共工具如解析启动参数、plat平台代码、drivers各种外设驱动、libAArch64的一些底层库如E1/E2/E3上下文管理、锁、延迟等、services运行时服务的具体实现、include头文件和fdts设备树片段。其中plat/下是最值得花时间的。每个平台一个目录里面再细分common、drivers和具体SoC相关代码。以plat/arm/board/juno为例你能看到platform.mk、plat_common.c、plat_bl31_common.c这些文件它们完整定义了一块板子在ATF眼里长什么样——支持几个CPU核、Console用哪个地址、内存怎么分块、安全区域在哪个地址范围内、SPI中断怎么分配等等。services/std_svc/psci/和services/spd/这两个目录也很关键。前者是标准PSCI服务后者是Secure Payload Dispatcher每一种TEEOP-TEE、Trusty、QSEE等都对应一个SPD模块作用就是把标准SMC呼叫路由给对应的TEE。读ATF源码想理解安全世界如何与普通世界互通看SPD模块是最直接的切入口。3.2 编译系统与平台宏定义ATF的编译体系延续了Linux内核那套Kconfig/make风格核心是make加平台宏。常见组合是这样的make CROSS_COMPILEaarch64-linux-gnu- PLATjuno DEBUG1 V1CROSS_COMPILE指定交叉编译工具链前缀PLAT指定平台目录名DEBUG1表示编译带调试信息的版本再配合LOG_LEVEL50可以打开详细日志。定制的平台一般在plat/下新建目录然后重写platform.mk里的变量比如BL31_SOURCES、HW_ASSISTED_COHERENCY、USE_COHERENT_MEM等。我在实际移植时踩过一个编译上的坑默认的目标格式是BL31.elf但如果想让BL2可以校验它并将它加载到特定内存地址可能需要设置PRELOADED_BL33_BASE或者使用FIP工具把所有镜像打成一个包。FIPFirmware Image Package是ATF官方推荐的镜像打包方式它在BL2阶段被解析里面包含了BL31、BL32、BL33的镜像和对应的校验参数。用fiptool工具就可以生成FIP镜像命令大致是fiptool create --tb-fw build/xxx/release/bl2.bin --soc-fw build/xxx/release/bl31.bin --tos-fw tee.bin --nt-fw u-boot.bin fip.bin生成FIP之后BootROM直接加载FIP即可BL2会从这个包里解析各镜像。这种方式比手动指定多个加载地址要整洁得多也方便后期加入签名校验逻辑。3.3 安全启动审计要点签名、哈希与信任根做安全启动审计最关心的就是信任链上每一环的合法性。ATF支持两种主流的验签方式一种是RSA-PSS签名校验另一种是基于哈希的消息认证比如HMAC。由于现代平台基本都用RSA-2048或RSA-4096审计时重点看证书格式、签名填充方式和密钥在哪一步被加载。以我做过的一个项目为例BootROM第一级校验BL2时用的公钥hash烧在eFuse里BL2的镜像附带签名块校验流程是BootROM从eFuse读出公钥hash解开镜像里的证书验出公钥用公钥去解签名比对镜像摘要。这一步通过后BL2再去校验BL31和后续镜像。整个过程串成一条链任何一环被篡改后续校验都会失败。ATF在代码里对应的是drivers/auth/目录实际用到的主要是img_parser_mod.c和crypto_mod.c接口抽象得相当干净方便接不同的加密库mbedTLS、OpenSSL等。审计的时候还有几个容易漏的点要特别留意一是BL1跳转到BL2之前有没有清干净BL1用的栈和临时数据二是BL2加载盲校验的数据源是不是可被普通世界控制三是BL31的栈和堆是否落在安全DRAM区域内且被TZASC保护。这几个点如果处理不干净攻击者完全可能绕过签名校验直接注入恶意代码。4. 平台移植落地从零开始适配一块新开发板4.1 移植前的准备工作硬件特征梳理与最小启动目标拿到一块新开发板先别急着动ATF代码把硬件基础信息摸清楚比什么都重要。需要弄清楚的点包括CPU是哪个coreCortex-A72、A53还是自研核心cluster怎么组织DDR起始地址和大小串口控制器的基地址和时钟中断控制器是GIC-400还是GIC-500TrustZone是否支持TZASC有没有eFuse之类的一次性存储用于放密钥。这些信息决定了平台代码里需要配哪些参数。ATF里有个很典型的宏叫PLAT_PHY_ADDR_SPACE_SIZE它决定了EL3启用的辅助地址翻译表中的物理地址范围如果配小了DDR地址或外设地址访问就可能跳错。还有PLAT_MAX_PWR_LVL它表示电源域拓扑的最大层级一般开发板填2就够了core/cluster/system。移植的最小目标可以先定义为能编译通过BL31启动时串口打印日志然后通过PSCI把Linux内核启动起来。在这个目标达成之前其实不需要把TEE、安全启动全做完——安全功能可以后续叠加先把链路跑通再说。4.2 编写平台代码的典型路径以BL31为例ATF平台代码的主要入口函数在plat/xxx/plat_bl31_common.c和plat_xxx.c里。你需要实现的核心函数至少包括这几个bl31_early_platform_setup最早的平台初始化一般在这里配置串口和获取BL31入口参数。bl31_platform_setup在BL31主流程里调用通常做中断控制器初始化、划分安全/普通中断、注册电源操作回调。plat_get_next_bl_paramsBL31正常引导时获取下一阶段启动参数BL33入口地址、DDR信息透传等。plat_setup_topology描述CPU的拓扑结构返回亲和级别信息。PSCI相关回调比如platform_setup_pm、platform_cpu_off、platform_cpu_on_finish。拿串口初始化来举例一般做法是这样的static console_t console; void bl31_early_platform_setup(u_register_t arg0, u_register_t arg1, u_register_t arg2, u_register_t arg3) { console_16550_register(PLAT_UART_BASE, PLAT_UART_CLOCK_IN_HZ, PLAT_UART_BAUDRATE, console); }别小看这一步串口能打印日志之后你的调试效率会立竿见影。实际开发中我建议尽早让自己的console通起来否则后面BL31跑飞了连日志都没有排查问题全靠猜痛苦程度指数级上升。BL31平台代码写完编译时还需要在platform.mk里把对应源文件加进来同时设置必要的宏例如BL31_SOURCES plat/xxx/plat_bl31_common.c。这些细节文献里写得不详细但漏一个文件编译就过不了我一开始对这个make体系不熟卡了整整半天才弄明白是文件没加进去。4.3 电源域与PSCI回调的实现细节PSCI移植核心在于把ATF定义的电源操作回调绑定到你SoC实际的电源控制器驱动上。ATF定义了一组结构体叫plat_psci_ops成员包括cpu_standby、cpu_on、cpu_off、cpu_suspend、system_off等。在你的平台代码里初始化这个结构体并把它注册给PSCI服务后续BL31调用PSCI服务时会自动走到你的回调。例如platform_setup_pm函数里就需要做这么一件事int platform_setup_pm(const plat_psci_ops_t **psci_ops) { const plat_psci_ops_t *ops my_platform_psci_ops; *psci_ops ops; return 0; }而my_platform_cpu_on里调用的底层操作一般要组合几件事往电源控制器写寄存器让CPU退出复位状态配置GIC的电源管理比如设置GICR或GICD的使能以及准备好CPU唤醒后的入口地址。以我做过的一款自研芯片为例CPU_OFF的回调里还需要把该核对应的L1 cache打包flush掉否则下次唤醒时可能读到脏数据。这部分最容易出问题的地方有两个一个是对GIC与CPU电源的关系理解不够导致CPU再上电后中断状态错误另一个是suspend状态定义太复杂刚开始没必要支持深层睡眠先把CPU_OFF和ON的路径跑通再逐级加suspend支持否则调试面太大了。4.4 平台特定设备树与内存布局适配ATF虽然不完全依赖FDTFlattened Device Tree但BL31传递给BL33的启动参数里会携带DDR布局和可能需要的固件信息。Linux启动时会用这些信息来检测可用内存范围和设备资源。这里的关键点在于确保ATF编译时用的PLAT_DEF内存配置与实际硬件一致。ATF内部维护了一张内存映射表通过plat_get_mmap()函数返回。它描述哪些区域是设备内存、哪些是安全RAM、哪些在EL3阶段需要建立映射。如果这里的起始地址写错了BL31访问外设寄存器时直接触发同步异常系统卡在EL3里连日志都打不完整。我在某次移植时就吃过这个亏板子实际DDR起始地址是0x40000000但平台代码里写的还是0x80000000结果BL31跳到BL33时Linux直接起不来内存管理部分报了各种让人摸不着头脑的错误。最后把DDR配置对齐才彻底解决。类似这样的低级错误如果不把内存布局梳理清楚排查起来极其浪费时间。5. 常见问题与排查技巧实录5.1 BL31启动失败的通用排查路径遇到BL31起不来或者起不完整我一般按下面顺序排查首先看串口有没有输出。如果完全没有任何日志大概率是console配置错误或者BL31根本没被BootROM/预加载器正确搬进内存。检查PLAT_UART_BASE是否与硬件一致检查串口时钟是否配到寄存器里检查BL31的加载地址是否和链接地址一致——尤其是用了FIP打包后BL31可能不在默认地址运行链接脚本里有个BL31_BASE需要和实际load地址保持一致。如果有日志但卡在某个阶段看INFO和ERROR日志停在哪一行。常见的情况是MMU配置期间访问到了未映射地址这通常需要回查内存映射表。可以临时把DEBUG开起来并列印更多日志ATF的LOG_LEVEL在include/common/debug.h里定义调到50会输出非常多辅助信息能帮你快速定位是地址问题还是寄存器配置问题。如果日志显示BL31已经跳到BL33但系统卡死这时候问题大多不在EL3而在BL33侧。可以先从ATF侧拿到传给BL33的参数plat_get_next_bl_params注意返回值里包含了arg0~arg3确认BL33入口地址和DDR布局传递是否正确。很多时候内核起不来或者uboot起不来是因为ATF透传的数据没有按标准约定填充。5.2 PSCI调用异常的排查记录PSCI是对接内核最容易出问题的地方。常见现象是系统起来之后执行reboot直接挂死或者echo mem /sys/power/state睡眠后唤不醒。reboot挂死先确认ATF里SYSTEM_OFF和SYSTEM_RESET的回调有没有正确绑定到硬件复位控制器。有些平台的复位控制寄存器是安全的需要在EL3里访问如果权限配置不对会触发错误。睡眠唤不醒多数原因在于唤醒中断没有被正确配置为Secure中断或者GIC的电源管理配置不到位。排查方法是在suspend回调里加日志确认ARM Core真的跑到了WFI指令并且中断能进入EL3的处理流程。我自己有个很实用的经验刚开始调试PSCI时先禁用掉CPU idle的所有深度状态只保留WFI浅睡眠。等确认浅睡眠没问题后再慢慢开启C1/C2那些复杂状态不然一次涉及多个层级的调试会让你崩溃。把问题拆小是嵌入式开发里永远有效的原则。5.3 安全启动签名验签失败快速定位启用安全启动后常见场景是明明镜像没动过但启动到BL2验签时报告hash mismatch。遇到这种情况我一般会先列出三样东西镜像的原始内容、签名块内容、验签用的公钥hash。然后用openssl dgst手动算一下镜像摘要核对ATF内部计算的方式是否一致——特别是摘要算法是SHA-256还是SHA-384填充方式是否一致。还有一个容易忽视的点BL2的验签时机和镜像的加载地址可能不一致。比如BL31本来应该加载到0x60000000实际加载到了0x0ATF默认会根据镜像头里的描述符去计算地址你若在fiptool打包时没有把load address填对验签时算的是碎片内容签名自然对不上。解决方式是检查FIP里每个镜像的入口地址和load地址是否与链接脚本、平台宏保持一致。打印验签错误日志时注意看ATF返回的错误码比如-EAUTH认证失败和-ENOENT缺证书/镜像定位方向完全不同。前者基本是密钥或签名时间戳问题后者多半是FIP内容缺失或者是解析路径不对排查思路差异很大千万不要混在一起看。5.4 问题和排查速查表典型现象可能原因排错切入点BL31无任何串口日志console基址/时钟/波特率错误BL31加载地址不对确认串口寄存器地址、链接地址与加载地址BL31卡在MMU初始化平台内存映射表访问了未映射地址检查plat_get_mmap临时提高LOG_LEVEL跳转BL33后无打印传给BL33的参数异常或BL33入口不在预期地址检查plat_get_next_bl_params与BL33入口地址内核执行reboot死机PSCI系统复位回调异常确认EL3能访问复位控制寄存器睡眠后无法唤醒唤醒中断没配置成Secure中断或GIC配置错误检查GICR/GICD电源管理及中断路由配置验签失败公钥hash不一致签名填充错误load地址与签名不符核对镜像摘要、公钥hash和FIP装载地址6. 一次真实移植过程的复盘最后说说我最近一次做ATF移植的完整经历希望能帮你在思路上多一些代入感。那是一个基于四核Cortex-A55的量产芯片原先的方案是跳过ATF直接跑uboot但产品安全需求提上来之后必须在启动早期加入安全隔离和TEE能力。我当时的计划是先把ATF最小跑通实现BL31日志输出与PSCI启动内核然后第二步再接入OP-TEE第三步再启用安全启动。第一步比预想的顺利因为芯片厂商提供了很完整的平台参考代码我只需要对照自己板子的UART基址和DDR配置做少量修改。串口日志出来后我把它接上一个jtag调试器确认BL31进入EL3并且成功调起了内核。当时我感觉这东西也没多难结果到第二步就给我上了一课。接入OP-TEE时BL32镜像加进去了但BL31的SPD配置和OP-TEE的加载地址一直对不上。OP-TEE要求运行在安全DRAM区域需要通过DTC和ATF平台代码共同指定。我排查了很久最后发现是BL32的load地址与实际链接地址不一致OP-TEE的链接脚本里指定的地址比ATF的配置偏了2MB。手动把地址对齐后OP-TEE才稳稳跑起来。这次经历让我深刻体会到ATF打包启动链所有镜像的地址必须三条线一致——链接脚本、ATF平台宏、FIP打包参数少一条就崩得不明不白。第三步安全启动更麻烦要把eFuse密钥、证书格式、签名算法全部对齐。我参照官方文档设计了RSA-2048的信任链证书格式用标准X.509把BL2的公钥hash烧进eFuse之后验签流程才走通。期间踩的坑主要是openssl的签名填充模式与ATF预期不一致还有些字段顺序不对导致读出来的公钥是个乱码。后来我写了段脚本把证书生成、镜像签名、FIP打包做成一条流水线才算把这个流程稳定固化下来。整个过程前后用了大约两周时间最大的收获不是把功能调通了而是建立了对ATF各阶段行为和源码查找路径的一种“肌肉记忆”。现在拿到一块新板子我能快速判断问题出在BL1、BL2还是BL31也知道哪些坑一定要提前绕开。我个人在实操中最深的体会是ATF的源码一层层剥开很多地方的设计已经经过大量真实芯片验证所以遇到问题时不要急着怀疑ATF实现有bug先怀疑自己的平台配置。把平台代码里每个宏、每个函数都对照实际硬件去核对一遍大部分问题都能找到答案。还有一点调试工具能早接就早接JTAG加ATF日志的组合远比单纯靠加打印猜问题高效。等把一套流程跑顺了你会发现自己对整个ARM启动体系的理解也上了一个台阶。