提起Arm平台上的安全固件,ATF(Arm Trusted Firmware,现在官方叫TF-A)几乎是绕不开的存在。做嵌入式底层、固件安全、系统移植的工程师,一上来要面对的就是BL1、BL2、BL31、BL32、BL33这一串术语,以及一份看起来庞大又复杂的源码树。我最早接触到ATF是在做TEE和Secure Boot集成的时候,被文档里写的trusted boot chain绕得够呛,后来干脆一行行读源码,才把整个链路理顺。这篇文章会把我做源码审计和平台移植过程中积累的东西摊开来讲,适合两类人看:一类是想搞懂ATF内部逻辑、需要做安全固件工程审计的开发者,另一类是正在做新SoC平台移植、被各种启动失败折磨的嵌入式工程师。我不会讲太多PPT层面的概念,尽量直接落到代码和操作上。
1. 为什么ARM平台的安全固件绕不开ATF
1.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/BL33
ATF的启动过程可以理解为“层层接力验证”。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/EL2,OP-TEE跑在安全世界的S-EL1,而ATF自己待在EL3。两个世界之间不是随便跳的,必须通过SMC异常进入EL3,由ATF完成上下文保存和恢复、内存访问权限切换,再把控制权交给目标侧。
日常开发中最常接触的就是psci_cpu_on这类SMC请求。比如Linux要启动一个CPU核心,会发SMC到EL3,ATF的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到跑通BL31
4.1 移植前的准备工作清单
拿到一块新SoC,先把这几样资料备齐:SoC的TRM(Technical 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 PLAT=myboard bl31生成BL31镜像;另一条是完整启动,需要先生成证书,打包FIP。证书生成依赖cert_create工具,同时需要提供私钥和公钥。自测环境可以用开发密钥,量产则必须换成硬件信任根保护的密钥。
命令大致是这样:
构建ATF本身以及FIP的典型命令(qemu/qemu等平台通用逻辑):
# 先设置工具链 export CROSS_COMPILE=aarch64-linux-gnu- make PLAT=myboard DEBUG=1 bl31 # 生成platform key,测试用 cd tools/cert_create make PLAT=myboard ./cert_create -n -o debug_key.pem -t debug_key.pem # 打包FIP,加载U-Boot作为BL33 make PLAT=myboard TBBR=1 DEBUG=1 \ BL33=u-boot.bin \ TRUSTED_KEY_CERT=fip_output/trusted_key.crt \ fip实际项目里crt和key的管理很复杂,我建议先跑通非安全启动,也就是不开启TBBR验证,等整个平台能启动到BL33之后,再逐步打开签名验证。直接一上来就上全套Trusted Boot,排查问题的难度会翻好几倍。顺序很重要:先串口、再BL31、再FIP、最后开TBBR,每步都验证通过再往下走。
4.5 用FVP和QEMU做无硬件预验证
如果你手头还没有真实硬件,别慌,ATF官方提供了FVP(Fixed 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初始化。
另一个容易忽视的是编译选项。DEBUG=1和LOG_LEVEL的设置直接影响串口输出量,如果DEBUG关闭且LOG_LEVEL=40(LOG_LEVEL_NONE),那BL31起来后可能什么都不打印。把make命令加上DEBUG=1 LOG_LEVEL=50(LOG_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替换,平台宏变化经常导致二进制布局不一致。
这些都是实打实从调试现场积累出来的,每一句背后都是至少几个小时跟代码死磕的代价。拿出来分享,是希望你能少走这些弯路,把时间花在真正需要思考的架构和安全性问题上。