ATF源码深度解析:架构、安全审计与平台移植实战 📅 发布时间:2026/9/7 13:07:12 👁 浏览次数: 做ARM底层固件这几年我有个体会你对系统理解得有多深取决于你把EL3的地基看得有多透。ARMv8之后几乎所有高端SoC的启动链里都有一个绕不开的名字——ATFARM Trusted Firmware现在官方习惯叫TF-A。它能管安全启动、能管CPU热插拔和电源状态切换、能帮你把OP-TEE这些安全世界组件拉起来也能决定你的U-Boot和内核什么时候开始跑。但翻遍网上文章大多是概念图加启动日志很少有人在源码层面把架构、安全边界、移植真正打通。这篇文章算是我的一份工程记录从ATF源码出发把架构全景、安全固件审计思路、平台移植落地的完整路径过一遍适合正在做BSP、TEE、可信启动或SoC bringup的工程师参考。你会看到每个关键模块为什么存在源码里哪些函数真正干了活以及移植一块新板子时最容易踩的坑。1. ATF的定位EL3不是开关是地基1.1 从异常模型到安全世界ATF运行在哪很多刚接触ATF的人会把它当成一个普通的bootloader这是最大的误解。ARMv8-A的异常模型有四个特权级EL0是应用层EL1是操作系统内核EL2是虚拟化管理程序EL3则是最高特权层只有EL3能把系统在安全世界Secure World和普通世界Normal World之间来回搬。ATF就常驻在EL3它既不是U-Boot那种引导完就退场的角色也不是内核那样跑起来就不管的角色而是伴随系统整个生命周期的常驻底层服务。理解这一点很关键。你可以在没有ATF的情况下用U-Boot直接引导内核但这意味着放弃了安全启动、放弃了标准PSCI电源管理、放弃了TEE的加载与切换。现在的SoC复杂度和ARMv8的异常模型决定了EL3这块地基你绕不开。ATF做的事情本质上是把EL3的裸能力封装成一套可扩展的固件框架让安全固件、TEE OS、普通世界固件各就各位。1.2 ATF与U-Boot、内核的分工边界用一张表格能说清楚ATF和传统启动组件之间的职责边界组件运行层级核心职责生命周期Boot ROMEL3出厂固化加载并校验BL1上电后极短时间ATF BL1/BL2EL3建立信任根加载并校验后续镜像引导阶段ATF BL31EL3PSCI实现、SMC调度、安全世界切换常驻OP-TEE等TEE OSS-EL1/S-EL0提供安全服务、密钥管理、可信应用常驻U-Boot/UEFIEL2/EL1加载内核、设备初始化、Bootloader功能引导阶段Linux KernelEL1/EL0操作系统、驱动、应用常驻我见过很多团队在早期设计时问能不能不要ATF直接用U-Boot的fiq处理代替。理论上小核单片机可以这么做但在多核ARMv8平台上PSCI标准、CPU hotplug、系统挂起恢复这些操作都需要EL3代码来托管自己用裸汇编实现一套完全不现实。ATF的存在本质上就是把这些公共的EL3能力产品化、开源化你不用重新发明轮子。2. 架构全景从BL1到BL33的接力赛2.1 引导链拆解五个镜像的各自使命ATF的经典引导链是一个接力过程每一步都由前一级加载并校验后一级。很多人喜欢记BL编号但容易混淆谁加载谁、谁校验谁。我按实际执行顺序拆一遍BL1上电后由Boot ROM加载到SRAM里的第一段固件它负责建立最小执行环境包括设置异常向量、初始化一些基础外设然后从Flash或者其它存储里加载BL2。BL1是信任根的起点如果它被攻破后面全完所以通常放在片上ROM或受保护的SRAM里。BL2可信引导固件运行在EL3负责从存储介质加载BL31、BL32可选和BL33而且每个镜像都要验签。BL2像是一个把关人所有后续启动镜像的安全属性都在它这里确立。BL31EL3运行时固件当引导完成进入操作系统后BL31并没有退出它留在EL3处理SMC请求、实现PSCI、负责安全世界和普通世界之间的context切换。BL32可选的安全世界OS最常见的实现是OP-TEE运行在S-EL1。ARM平台的标准做法是让BL31作为BL32的宿主通过Standard Secure Service调度的方式把请求转发给TEE。BL33最终要启动的普通世界镜像通常就是U-Boot或UEFI也可以是直接引导的Linux内核镜像。用一张表格可以看得更清楚镜像加载者运行特权级主要职责是否常驻BL1Boot ROMEL3建立信任根、加载BL2否BL2BL1EL3加载并验证BL31/BL32/BL33否BL31BL2EL3PSCI、SMC调度、安全世界切换是BL32BL2S-EL1TEE安全服务是BL33BL2EL2/EL1普通世界Bootloader或内核引导阶段这里面容易忽略的一点是BL2并不是必须要深入运行到操作系统阶段的。在很多量产SoC上BL2加载完BL31和BL33之后它的工作就完成了可以回收内存甚至断电。所以内存布局设计时要考虑BL2和BL31是否共用同一个SRAM区域如果共用BL2退出后占用的空间就能还给BL31使用。2.2 EL3常驻运行时SMC/PSCI是如何工作的BL31作为常驻固件核心工作之一是处理SMC异常。当普通世界的代码想请求安全服务时会执行smc指令带着一个函数ID和若干参数陷入EL3。ATF在bl31/aarch64/runtime_exceptions.S里统一处理这个异常然后把请求分发给对应的Runtime Service。整个分发的几条路径是Standard Service比如OP-TEE标准调用U-Boot或内核通过标准SMC访问TEE功能。PSCI Service电源管理相关比如CPU上下电、系统重启。Vendor ServiceSoC厂商自定义服务比如厂商的底层控制接口。函数ID本身不是随便定的ARM定义了调用约定bit31标记是Fast Call还是Standard Callbit30标记是64位还是32位调用高16位是服务ID低16位是具体函数编号。这种编码方式保证了SMC接口可以扩展同时让ATF在smc_dispatch时能快速路由到对应服务。以PSCI为例Linux内核在CPU热插拔时会调用PSCI_CPU_ON这个调用从EL1发起陷入EL3后BL31的PSCI实现负责配置目标CPU的启动地址、释放其复位信号然后让目标CPU从安全启动地址开始运行。这一整套过程如果你绕过ATF自己写需要处理的中断、缓存、亲和性细节会非常折磨人。ATF把这些封装成一个标准接口这也是为什么现在几乎所有ARM64 Linux发行版都依赖PSCI。2.3 源码目录结构与关键代码地图做源码评测和审计必须对代码目录有肌肉记忆。ATF的源码顶层结构如下bl1/ BL1实现 bl2/ BL2实现 bl31/ BL31实现包括运行时服务框架 plat/ SoC和板级平台代码 services/ runtime servicestd_svc、spd等 lib/ 公共库包括el3_runtime、psci、pmf、xlat_tables drivers/ 外设驱动包括console、timer、gpio等 include/ 公共头文件 make_helpers/ 构建辅助脚本 tools/ 辅助工具如fiptool、cert_create最关键的几个代码文件我建议按这个顺序读bl31/bl31_main.cBL31入口逻辑看它如何初始化上下文、注册服务、进入主循环。bl31/aarch64/runtime_exceptions.SEL3异常向量SMC陷入的入口。services/std_svc/psci/psci_main.cPSCI主分发逻辑。lib/el3_runtime/aarch64/context_mgmt.c安全世界和普通世界的上下文保存与恢复。plat/common/plat_psci_common.c平台相关的PSCI公共实现。读ATF源码有一个技巧不要从头读到尾先把BL31主线抠出来然后顺着runtime_svc_init找到服务注册表再看一条SMC请求怎么被分发出去。这条线通了整个框架就能立住。3. 安全固件工程审计从源码里找问题3.1 信任链与镜像校验安全启动的根基ATF的安全价值很大程度体现在信任链上。信任链的本质是每个镜像在加载下一个镜像之前都对它做完整性验证和来源认证。ATF源码中通过auth模块实现常见流程是BL1从Boot ROM拿到BL2镜像后用存储在OTP或eFuse中的根公钥哈希来验证BL2的签名。验证通过后BL2才有资格继续加载BL31。审计时需要重点确认点位根公钥有没有写死、能不能被替换。如果平台允许在调试模式下跳过验签量产固件必须关闭这个通路。镜像的签名算法和密钥长度是否满足当前安全基线比如是否还在用SHA-1、是否使用固定公钥。校验失败后的处理路径失败分支是否会导致进入不可控状态比如回退到可调试模式。我在审计中经常发现的问题是有些平台为了开发方便默认编译了ENABLE_DEBUG或跳过验签的选项一旦忘记关掉整个信任链就形同虚设。这个问题在量产固件里是致命的却是工程中最常见的失误。3.2 内存隔离与外围设备访问控制安全固件要保证普通世界不能随意读取安全内存和外设。ATF提供的是从EL3视角配置的地址空间隔离核心机制是TrustZone地址空间控制器TZC-400/TZASC。审计时主要看内存映射里安全区域和非安全区域的划分是否正确尤其要盯着DDR的address region配置有没有多划、漏划。MMU内存属性是否设成了DEVICE或NON_CACHEABLE安全世界访问外设时的内存屏障是否合理。BL31的代码和数据是否放在受TZC保护的SRAM中如果放在DDR且DDR区域没有隔离普通世界一旦拿到DMA能力漏洞面就大了。ATF源码里和内存配置强相关的是plat_get_next_bl_params和plat_get_mmap_entries这两个函数会把平台的内存映射信息交给BL阶段使用。做审计时我会把平台Makefile里的PLAT_PHY_ADDR_SPACE_SIZE和PLAT_VIRT_ADDR_SPACE_SIZE配置与TRM里的实际地址空间比对看有没有越界映射。这里有个很容易踩的坑虚拟地址空间和物理地址空间大小不一致时地址翻译表配置不当会直接导致页表预填充失败启动时一脸茫然。3.3 SMC接口的边界审计SMC接口是普通世界和安全世界之间的唯一合法入口也是安全攻击面的核心。一个设计的SMC处理函数如果校验不足攻击者完全可以通过普通世界构造恶意参数来触发安全世界越界访问。审计SMC接口时我有一套固定动作检查函数ID是否严格按平台允许列表匹配有没有通配或模糊匹配。检查指针参数是否用load_with_validation或等效机制做过安全校验不能直接解引用普通世界传入的地址。检查长度参数是否计算正确尤其看有没有整数溢出。一个经典的漏洞模式是len arg0 arg1时加法溢出后长度变小导致后面的拷贝越界。检查返回值处理安全世界有没有把敏感状态信息通过返回值泄露给普通世界。我见过一个真实案例某厂商的vendor service在处理DMA描述符时直接用普通世界传入的地址作为源地址且没有校验地址范围导致普通世界可以通过构建特定描述符读取任意安全内存。这个问题在静态代码扫描时不容易发现核心原因是调用链太深必须靠审计人员对数据流做跟踪。3.4 静态分析与常被忽略的缺陷类型ATF本身经历了比较长时间的开源打磨代码质量整体在线但平台层代码往往是薄弱点。因为平台层由SoC厂商开发质量差异很大。我通常用两种方式做审计第一种是人工代码走读聚焦在plat/目录下的安全敏感函数上。第二种是借助静态分析工具做辅助包括但不限于Coverity、Clang Static Analyzer、还有开源圈子常用的cppcheck。静态分析能抓出空指针解引用、资源泄漏、死代码这类基础问题但真正严重的安全问题往往要人工分析数据流才能发现。我一直提醒团队的人的审计重点清单审计点重点关注典型风险启动信任链根密钥存储、验签路径调试跳过验签未关闭内存隔离TZC配置、物理/虚拟地址空间安全内存暴露给普通世界SMC接口参数校验、指针安全、整数溢出任意内存读写上下文切换寄存器保存恢复完整性安全世界寄存器泄露异常处理同步异常路径、失败处理拒绝服务或权限提升4. 平台移植落地指南从参考板到新板子4.1 移植前需要准备好的资料平台移植最容易犯的错是想都不想就开始写代码。我习惯第一步把所有资料备齐再撸起袖子。你至少需要目标SoC的Technical Reference Manual重点看内存映射和启动流程。参考板的设备树和电路图搞清楚串口、电源管理、中断控制器接了哪些外设。ATF源码里已有的同系列平台代码比如你的新平台和某款参考SoC外设兼容度高那能从参考平台改起会省很多事。ARM官方docs/porting-guide.md要有这份文档虽然老了点但很多接口说明到今天依然有效。4.2 新建平台目录与基础配置在ATF源码里新建平台目录结构一般这样组织plat/vendor/board/ ├── board.mk ├── platform.mk ├── plat_def.h ├── plat_private.h ├── ...platform.mk是构建系统能认出这个平台的关键里面声明平台名、编译选项、包含哪些源文件。打开一个现有平台做模板改下面几项最快PLAT名称比如PLAT : myboard。架构相关配置比如ARM_ARCH_MAJOR : 8如果你的SoC是ARMv8.2就设成8对应特性的ARM_ARCH_MINOR也要配套。地址空间配置PLAT_PHY_ADDR_SPACE_SIZE和PLAT_VIRT_ADDR_SPACE_SIZE。这两个值直接决定了地址翻译表的规模通常物理和虚拟都设成1ULL 32但如果DDR很大或有外设映射在高位就得调整。核心数配置PLATFORM_CORE_COUNT、PLATFORM_MAX_CPUS_PER_CLUSTER这些宏影响后续PSCI和CPU启停逻辑。这里要提醒一个细节PLAT_PHY_ADDR_SPACE_SIZE不是越大越好它决定了ATF使用xlat table时预留的表空间大小。设得太大会让BL31镜像变大甚至超出SRAM资源设得太小则地址翻译覆盖不了整个DDR空间启动半路就出异常。4.3 必须实现的核心接口ATF的平台层有一套接口矩阵移植时如果只实现了一部分通常会在对应阶段编译失败或启动失败。以BL31为例核心接口包括bl31_early_platform_setup最早期初始化必须在MMU和缓存开启前设置好控制台、BL31参数。bl31_plat_arch_setup配置页表、使能MMU、初始化串口。bl31_platform_setup初始化BL31需要的外设、配置中断控制器等。plat_get_next_bl_params设置BL31传给BL33的参数包括设备树地址、跳转地址。platform_setup平台自定义的电源管理配置回调。PSCI相关的plat_psci_ops结构体实现CPU_ON、CPU_OFF、SYSTEM_OFF等操作。很多第一次移植的人会在bl31_plat_arch_setup这卡住。这个函数要正确配置ARM核心的内存翻译表尤其是mmap_add_region添加串口和外设区域。如果串口的物理地址映射错了BL31起不来你连一个log都看不到。4.4 编译集成与FIP打包平台代码写好之后编译命令形如make PLATmyboard DEBUG1 LOG_LEVEL40 \ ARM_ARCH_MAJOR8 \ bl31LOG_LEVEL40能让你在调试阶段看到完整日志默认的LOG_LEVEL可能只有20很多细节会被藏掉。编译产物中build/myboard/debug/bl31/bl31.elf是可调式镜像bl31.bin是烧录用镜像。要组成可启动镜像还得用FIP工具把所有镜像打包。ATF的打包命令大致是make PLATmyboard all tools/fiptool/fiptool create \ --tb-fw build/myboard/debug/bl2/bl2.bin \ --soc-fw build/myboard/debug/bl31/bl31.bin \ --nt-fw build/myboard/debug/bl33/u-boot.bin \ fip.binFIPFirmware Image Package是BL2加载时的标准镜像格式它把BL2、BL31、BL32、BL33等串在一起并在头部保存每个镜像的偏移和大小。如果你的启动媒体是eMMC一般把fip.bin写到固定分区Boot ROM先加载BL1BL1再加载BL2BL2解析FIP并加载其它镜像。4.5 最小可启动记录我做一个新板移植时遵循先亮串口再跑BL31最后接BL33的顺序先确保BL1能跑起来串口能打第一行日志。再调BL2到BL31的链路确保BL31跳转成功。最后再接U-Boot验证PSCI调用是否正常。有一次我在一块新板子上折腾了三天最后发现是plat_def.h里的UART基地址写错了串口一直输出乱码而不是SPL日志。从那以后我养成了一个习惯正式写平台代码之前先在草稿纸上把SoC memory map里的UART地址、GIC地址、复位向量地址列出来放到编译器能直接看的宏里而不是散落在各个文件里面。5. 常见问题与调试实录5.1 启动卡死与打印丢失的排查顺序ATF启动阶段如果卡死最让人头疼的问题是什么输出都没有。这时候我会按这个顺序排查确认串口物理连接和控制台配置ATF早期console初始化依赖平台代码如果UART地址不对或波特率不对不会有任何输出。确认BL1是否真的从Boot ROM加载到了预期位置如果BL1大小超了SRAM会在加载阶段被截断。确认MMU使能之后代码是否还能正确访问外设。MMU开启前后内存属性不一致会导致串口瞬间消失。如果BL31跳转后卡住优先检查EL3异常向量表是否设置正确以及BL31镜像是否超出了TZRAM地址范围。调试时我建议先开DEBUG1和LOG_LEVEL40配合JTAG读取PC值定位卡住的位置。没有JTAG的话就在关键函数入口加NOTICE日志二分定位卡点。5.2 SMC/PSCI异常定位如果操作系统起来之后某个CPU热插拔操作失败或者系统重启不生效问题往往出在PSCI实现或SMC分发路径上。调试经验是先构造一个最简单的SMC调用比如PSCI_VERSION如果它都返回失败说明BL31的服务注册或SMC dispatch链路就没通。检查中断是否拥塞。ATF在bl31_main里默认会启用一些中断如果你在disable_interrupt和enable_interrupt之间用了很长延时可能会导致SMC请求被中断打断行为异常。检查平台plat_psci_ops是否都赋值完整有一个回调缺失PSCI调用会直接进入default分支返回错误。5.3 编译与链接问题速查表现象可能原因排查方向BL31_IMAGE超限TZRAM容量不足调整PLAT_PHY_ADDR_SPACE_SIZE或精简BL31功能链接时找不到plat_XXX函数平台接口未实现对照porting-guide.md补全接口启动即死看不到任何日志控制台没初始化或UART地址错误检查plat_def.h和console_init配置SMC调用返回NOT_SUPPORTEDRuntime Service未注册检查declare_rt_svc宏和服务编号多核启动后从核卡死高核启动地址或cache配置错误检查PSCI的CPU_ON实现和启动装配地址我用这张表当急诊手册用了好几年每次碰到新平台的诡异问题先对着表格逐项排除能省不少时间。最后再分享一个小技巧移植ATF时别一上来就追求全功能先把最小启动链路跑通在BL31里加一条NOTICE(hello atf)日志确认EL3世界能正常工作再逐步加入TEE、安全启动、厂商扩展服务。后面的路会顺畅很多。这套固件的水很深但把架构、审计、移植三件事拆开来看你会发现每件事都有清晰的路径。