ATF源码深度解析:ARM安全固件启动链与平台移植实战 📅 发布时间:2026/9/7 18:49:15 👁 浏览次数: 提起Arm平台上的安全固件ATFArm Trusted Firmware现在官方叫TF-A几乎是绕不开的存在。做嵌入式底层、固件安全、系统移植的工程师一上来要面对的就是BL1、BL2、BL31、BL32、BL33这一串术语以及一份看起来庞大又复杂的源码树。我最早接触到ATF是在做TEE和Secure Boot集成的时候被文档里写的trusted boot chain绕得够呛后来干脆一行行读源码才把整个链路理顺。这篇文章会把我做源码审计和平台移植过程中积累的东西摊开来讲适合两类人看一类是想搞懂ATF内部逻辑、需要做安全固件工程审计的开发者另一类是正在做新SoC平台移植、被各种启动失败折磨的嵌入式工程师。我不会讲太多PPT层面的概念尽量直接落到代码和操作上。1. 为什么ARM平台的安全固件绕不开ATF1.1 EL3异常级别只有ATF这一个“合法玩家”Armv8-A引入的异常级别模型把系统分成了EL0到EL3四层EL0跑普通应用EL1跑操作系统内核EL2跑虚拟化EL3则是最底层的安全固件。这个层级设计不是摆设它是整个Arm安全模型的核心。EL3能访问绝大多数硬件资源能决定非安全世界能不能访问某个外设也能在安全世界和非安全世界之间切换上下文。换句话说EL3就是整个系统里权限最高的“裁判”。ATF就是这个EL3层级的标准参考实现。你可以在它基础上改也可以把EL3固件整个换掉但绝大多数商业项目都选择在ATF之上做定制因为Arm自己维护着完整的驱动、安全启动和PSCI电源管理实现从零写一个EL3固件的成本太高了而且错误一旦上线就是安全事件。这也是我把ATF列为安全固件审计第一站的最直接原因。1.2 ATF到底解决了哪些实际问题从功能上说ATF干的事情有三大块。第一块是安全启动它负责建立从芯片上电到OS加载之前的信任链保证每个加载进来的镜像都是经过签名验证的。第二块是运行时服务系统启动完成之后ATF常驻在EL3通过SMC指令对外提供服务其中最重要的就是PSCI电源管理比如CPU的上电下电、系统的重启和关机。第三块是安全世界与非安全世界之间的切换和通信移动设备里的TEE如OP-TEE就是由ATF加载和调度的。这三个能力基本覆盖了嵌入式设备的整个生命周期开机要验证运行中要管电源还要调度安全服务。理解了这三点再去读源码心里就有谱了不会一头扎进细节出不来。1.3 源码评测选什么版本ATF仓库更新很快不同版本之间的接口变化也不小。我这次评测基于v2.8到v2.9左右的代码这两个版本在主流SoC平台和编译工具链上有大量实际验证社区资料也全。如果你正在做平台移植建议先锁定一个LTS倾向的稳定版本不要直接追master。等整个平台跑通了再考虑升级到新版本否则很容易被上游改动打乱节奏。2. ATF架构全景从启动链到运行时服务2.1 一段话讲清楚BL1/BL2/BL31/BL32/BL33ATF的启动过程可以理解为“层层接力验证”。BL1是整个系统的信任根代码通常固化在芯片ROM里上电后它先初始化最基础的硬件环境然后把BL2镜像加载到SRAM并验证。BL2是可信启动固件它负责从FIP包中把BL31、BL32和BL33加载出来并且逐一验证签名。BL31是EL3的运行时固件启动校验完成后一直驻留在EL3处理各种SMC请求。BL32通常是TEE OS比如OP-TEE。BL33是下一级引导程序最常见的是U-Boot或UEFI。这个接力过程里最关键的词是“验证”。每一级都只信任上一级验证过的镜像信任根在BL1的ROM代码里密钥则通过芯片的OTP fuses注入。一旦链路建立起来任何一环被篡改都会导致启动失败这也就是Trusted Board Boot的基本逻辑。2.2 EL3、S-EL1和S-EL2两条世界之间的调度除了异常级别ATF还要处理Arm TrustZone的“安全世界”和“非安全世界”两个概念。Linux跑在非安全世界的EL1/EL2OP-TEE跑在安全世界的S-EL1而ATF自己待在EL3。两个世界之间不是随便跳的必须通过SMC异常进入EL3由ATF完成上下文保存和恢复、内存访问权限切换再把控制权交给目标侧。日常开发中最常接触的就是psci_cpu_on这类SMC请求。比如Linux要启动一个CPU核心会发SMC到EL3ATF的PSCI服务收到请求后验证参数、初始化目标核心然后设置该核心的入口地址让它跳进Linux的启动代码。整个过程里面涉及保存寄存器、配置异常向量、设置内存属性任何一步错都可能导致系统挂死或者安全漏洞。2.3 源码目录结构与构建骨架ATF源码的顶层结构非常有规律这也是我特别喜欢拿它做工程范例的原因。bl1、bl2、bl31这些目录对应各启动阶段的实现plat目录存放平台相关代码你的移植工作基本都在这里drivers目录下面是驱动GIC、UART、IO内存这些都有现成实现lib目录放公共库包括el3_runtime、el3_context_mgmt这些核心模块tools目录则有fiptool和cert_create这类构建工具。构建体系基于make每个平台在plat/厂商/平台/下维护自己的platform.mk指定要编译哪些源文件、用哪个链接脚本、配置哪些宏。构建时会用fiptool把BL31、BL32、BL33打包进一个FIP文件BL2再从FIP里提取对应镜像加载。理解这个目录逻辑之后无论是审计还是移植你都能快速定位到自己要看的文件。3. 安全固件工程审计ATF代码里到底在保护什么3.1 信任链审计从OTP密钥到各阶段镜像验证做安全固件审计我第一个看的就是信任链实现。ATF的Trusted Board Boot支持两种认证方式一种叫Auth_FW使用RSA或ECDSA签名一种叫Auth_OPTEE使用OP-TEE的签名方案。实际项目中用的最多的是RSA SHA256组合。构建时cert_create工具生成各级证书fiptool把证书和镜像打进FIP启动时每一级BL用自己的公钥验证下一级BL的证书链最后再验镜像哈希和签名。审计时要重点确认三件事。第一信任根密钥是否真的烧进了OTP而不是编译期写死在镜像里第二验证逻辑有没有绕过路径比如是否存在某个配置可以直接跳过签名检查第三回滚保护是否生效很多设备安全漏洞出在系统可以降级到旧版本固件上ATF里的修订计数器和anti-rollback机制就是干这个的。查源码的时候重点看drivers/auth目录以及每个平台对ARM_ROTPK_LOCATION的定义。3.2 内存隔离与权限控制是审计的高危区EL3固件最怕的问题就是内存越权。ATF自己驻留在Trusted RAM和Trusted ROM里通过TZC secure memory controller保护。审计时要确认BL31用的内存区域有没有被错误映射成非安全属性还要检查MMU的translation table配置。我习惯先看平台内存映射表再逐条核对内存属性凡是非安全世界能访问到的EL3数据都是重大安全隐患。另一条线是SMC接口的权限校验。ATF对外暴露了很多标准SMC服务比如PSCI、SDEI、SiP如果某个接口没有做调用方来源检查恶意普通世界的代码就能通过SMC拿到额外权限。检查方法也不复杂找到每个服务的handler入口看它在处理请求前有没有校验调用者的exception level和security state有没有对传入参数做范围检查这两点一旦缺失就是可以入库的漏洞点位。3.3 上下文切换中的寄存器泄露风险安全世界和非安全世界切换时寄存器状态必须完整保存和恢复。ATF里el3_context_mgmt模块负责这摊事审计时我会特别关注保存的寄存器集合是否完整有没有把安全侧的敏感寄存器遗漏。更重要的是恢复context的时候是否覆盖了全部通用寄存器、系统寄存器以及浮点寄存器。如果恢复不全就会出现信息泄露非安全世界通过侧信道读回安全世界残留数据。我在审计一个合作方固件时就发现他们的BL31修改过context保存逻辑为了性能把fpregs的保存去掉了结果OP-TEE里的密钥材料在切换后留在了FPU寄存器里。这类问题光看文档很难发现必须结合diff和运行时寄存器转储才能定位。3.4 审计过程中总结的高危Checklist为了方便后续审计和自查我把工程审计中碰过的高频风险点整理成了一张清单每次上手新固件都按这个顺序过一遍审计项重点关注常见风险信任链起点ROTPK来源、OTP fuse策略密钥写死编译目录无防回滚证书链验证每级BL的认证路径跳过或弱化次级验证SMC接口参数检查、调用来源检查缺权限校验可被普通世界调用内存映射MMU表格、TZC配置Trusted区被映射成非安全Context切换寄存器保存恢复完整性漏存或未恢复敏感寄存器中断路由GIC安全配置安全中断误路由到非安全侧这张表不复杂但每次审计都能筛出点东西。做安全固件工程永远假设敌手有能力篡改普通世界的任意代码EL3固件就是最后的防线所有边界都要按最坏情况去验。4. 平台移植落地指南从一个新SoC到跑通BL314.1 移植前的准备工作清单拿到一块新SoC先把这几样资料备齐SoC的TRMTechnical Reference Manual至少要包含内存映射、GIC配置和UART基地址参考板子的原理图确认串口、电源控制和复位逻辑GIC版本信息是GICv2还是GICv3这直接影响代码路径最后是芯片厂商如果有现成release强烈建议先拿官方的ATF分支跑通再对比mainline。资料备齐之后确认编译工具链。ATF官方推荐armclang或GCC社区用得最多的是aarch64-linux-gnu-gcc。这里提一个容易踩的坑交叉编译器版本太老或太新都可能引发链接脚本兼容问题我建议先用系统包管理器里的最新稳定版出问题再降级不要一开始就在工具链上较劲。构建时用make PLAT你的平台工具链通过CROSS_COMPILE指定。4.2 新建平台目录和platform_def.h核心定义以qemu平台为模板我会在plat/目录下新建plat/myboard/myboard目录然后创建platform_def.h、platform.mk、plat_topology.c这三个基础文件。platform_def.h负责定义内存地址和物理基址MAC常量例如BL31_BASE、BL31_LIMIT、UART_BASE、GICD_BASE、GICC_BASE还有PLAT_PHY_ADDR_SPACE_SIZE等。这些地址必须和SoC手册完全一致差一个bit启动就挂。实际写的时候先抄一个结构相近的现有平台比如qemu或fvp然后把地址全部替换成自己SoC的地址。我见过很多人上来就写很多初始化逻辑结果只是platform_def.h里的UART地址写错卡在串口无输出上查了一天。记住平台移植第一步是让串口能输出UART地址正确性优先于一切。4.3 BL2到BL31的加载验证逻辑怎么接平台目录里最常见的是plat_get_bl31_params和plat_get_next_bl_params这类接口的实现。它们负责把BL2解析出来的镜像信息传递给BL31。新平台如果不做特殊定制直接复用common代码里基于param结构的默认实现即可。真正需要自己写的是平台初始化函数比如bl31_platform_setup里要配置GIC、初始化串口、建立MMU映射。GIC的初始化是移植里最烦人的部分。GICv2比较简单初始化Distributor和CPU Interface就行GICv3则要处理Re-distributor、LPI、ITS这些复杂机制。ATF自带drivers/arm/gic/v3驱动平台代码里只需提供GICD_BASE、GICR_BASE等基址并调用gicv3_driver_init然后gicv3_distif_init。注意别漏掉对GICR的校验否则中断路由会静默失败系统看起来能跑但外部中断一个都进不来。4.4 完整构建和FIP镜像打包移植完成后的构建流程一般是两条线。一条是纯函数级验证直接make PLATmyboard bl31生成BL31镜像另一条是完整启动需要先生成证书打包FIP。证书生成依赖cert_create工具同时需要提供私钥和公钥。自测环境可以用开发密钥量产则必须换成硬件信任根保护的密钥。命令大致是这样构建ATF本身以及FIP的典型命令qemu/qemu等平台通用逻辑# 先设置工具链 export CROSS_COMPILEaarch64-linux-gnu- make PLATmyboard DEBUG1 bl31 # 生成platform key测试用 cd tools/cert_create make PLATmyboard ./cert_create -n -o debug_key.pem -t debug_key.pem # 打包FIP加载U-Boot作为BL33 make PLATmyboard TBBR1 DEBUG1 \ BL33u-boot.bin \ TRUSTED_KEY_CERTfip_output/trusted_key.crt \ fip实际项目里crt和key的管理很复杂我建议先跑通非安全启动也就是不开启TBBR验证等整个平台能启动到BL33之后再逐步打开签名验证。直接一上来就上全套Trusted Boot排查问题的难度会翻好几倍。顺序很重要先串口、再BL31、再FIP、最后开TBBR每步都验证通过再往下走。4.5 用FVP和QEMU做无硬件预验证如果你手头还没有真实硬件别慌ATF官方提供了FVPFixed Virtual Platform模型QEMU也有virt平台支持。这两个环境不仅能跑完整的BL1到BL33启动链还能配合调试器做断点和寄存器查看。我强烈建议先把代码在FVP上跑通再迁移到真实SoC很多明显的逻辑错误和地址错误在模型上会立刻暴露出来。FVP上调试的经典技巧是用串口输出加断言。ATF本身有非常多的assert打开后任何MMU配置错误、内存越界都会快速崩溃并留下线索。配合GDB远程调试可以直接停在异常向量入口通过ESR_EL3寄存器判断异常类型定位到具体是MMU fault还是SMC错误这套流程在真实板子上也适用。5. 常见问题与排查技巧实录5.1 串口无输出先把输出环境做对ATF移植遇到的第一个高频问题就是串口完全没输出。大多数情况不是代码逻辑错误而是platform_def.h里的UART地址和真实SoC不一致或者UART的时钟分频没配上。我一般会先做一次极简验证用汇编或极简C直接向UART硬件寄存器的FIFO写字符确认硬件通路可用再回头看ATF初始化。另一个容易忽视的是编译选项。DEBUG1和LOG_LEVEL的设置直接影响串口输出量如果DEBUG关闭且LOG_LEVEL40LOG_LEVEL_NONE那BL31起来后可能什么都不打印。把make命令加上DEBUG1 LOG_LEVEL50LOG_LEVEL_VERBOSE能看到大量平台初始化和请求流转信息排查问题速度快得多。5.2 BL31加载后立即崩溃的定位思路BL31崩溃比串口无输出好定位一些因为可能已经打印了部分日志。常见原因有三种BL31镜像的加载地址和链接地址不一致导致PC跑飞GIC初始化时访问了未映射的寄存器地址MMU配置把代码区域设错成不可执行或不可读。前两种都会伴随ESR_EL3异常值第三种则表现为PSTATE和返回地址异常。遇到这类问题我的排查顺序是先看崩溃时的fault address和ESR_EL3对照异常类型表确定是数据异常还是指令异常然后反查链接脚本确认BL31_BASE和LMA是否匹配最后用GDB挂在qemu/FVP上把反汇编和寄存器打出来基本能锁定问题。一定不要凭感觉改代码ATF这类底层固件猜问题的成本极高。5.3 开启TBBR后启动失败的常见原因开启Trusted Board Boot后启动链路会多出证书加载和签名验证两个环节。最常见的失败是“Auth image failed”这类错误。先检查FIP里是否包含了证书再确认烧进SoC的ROTPK是否和生成FIP时使用的公钥一致。这两个根因占了TBBR失败案例的八成以上。另外回滚保护的设计也容易出状况。如果BL1的修订计数器比BL2镜像高BL2会无法加载导致平台卡在启动早期。排查办法是检查NV counters值必要时清空或重布防值。要注意不同厂商对counter存储的实现差距很大有的存在OTP有的存算改区操作前一定要确认机制否则改坏就是永久变砖。5.4 几个值得存进笔记的调试技巧最后分享几个我实际项目里反复用到的调试技巧。第一ATF支持通过串口交互进入Console打开后可以手动执行一些调试命令查看内存和寄存器这在排查BL31状态时非常有用。第二fiptool的--dump参数可以直观看到FIP内的镜像和证书信息怀疑打包问题时先用它确认。第三加自己的打印日志时优先用NOTICE级别的宏别用INFO否则后续会被日志淹没。第四每次改动platform.mf文件后务必重新编整个fip不要只编bl31替换平台宏变化经常导致二进制布局不一致。这些都是实打实从调试现场积累出来的每一句背后都是至少几个小时跟代码死磕的代价。拿出来分享是希望你能少走这些弯路把时间花在真正需要思考的架构和安全性问题上。