ATF源码深度解析:安全固件架构、审计与平台移植实战指南

发布时间:2026/9/7 11:06:11
ATF源码深度解析:安全固件架构、审计与平台移植实战指南 很多做嵌入式或者安全启动的工程师第一次把 Arm Trusted FirmwareATF现在官方叫作 Trusted Firmware-A/TF-A的源码 clone 下来之后面对几十个 platform 目录、一整套 BL 编号体系还有各种宏定义基本都会愣一下这玩意儿到底从哪看起我最早碰 ATF 是在一个 ARMv8 平台的定制项目上系统起不来、console 没输出、PSCI 回调不回折腾了整整两周。回头看ATF 的源码并没有那么可怕只是它把“启动”“安全”“虚拟化”这些东西全压在一个很小的 EL3 世界里信息密度极高。这篇文章我打算把 ATF 的架构全景、安全固件审计思路、平台移植落地方法一条线讲清楚。不是泛泛读代码而是以“评估一份安全固件源码是否靠谱”和“把 ATF 移植到一个新板子上”这两个实际目标为线索带你从头到尾扫一遍。适合三类人第一类是 BSP 和固件工程师做平台移植第二类是安全工程师要做固件安全审计和启动链验证第三类是想搞懂 ARM 安全世界怎么运转的学生或爱好者。读完你至少能对 ATF 的代码结构、关键目录、移植步骤和常见坑有完整认识遇到问题也能自己翻源码排查了。1. 先从源码看 ATF 的整体架构全景1.1 三个 Bootloader 为什么拆开ATF 的核心设计就是“分级引导”把启动过程拆成 BL1、BL2、BL31 三个阶段。第一次看的人会问为什么不能像单片机那样一个 main 函数跑到底答案是安全和隔离。BL1 是上电后最早运行的一段代码通常被固化在 BootROM 或者非常小的 SRAM 里负责最基本的 CPU 初始化然后把 BL2 加载到可信内存并验证签名。BL2 是第二个阶段运行在 EL1/EL2负责初始化 DRAM 和外设从 flash 里把 BL31 和 BL33也就是 U-Boot、UEFI 这些非安全世界的引导镜像加载好。BL31 是关键它是真正长期驻留的 EL3 runtime提供安全监控、PSCI 电源管理、SMC 调度等服务。你可以把这三个阶段理解成一个安保流程BL1 是门口的保安只认自己的工作证验证完就放行 BL2BL2 是物业管家把所有住户各阶段镜像请进对应房间BL31 是小区中心后续所有住户都要通过它来申请服务。这种拆分保证了每个阶段的可信边界最小化也方便做证书链验证——每一级只信任上一级签发的镜像。1.2 EL3 运行时与 SMC 调度中枢BL31 起来之后代码不会退出而是进入一个死循环式的等待状态接收来自非安全世界的 SMCSecure Monitor Call请求。整个调度中枢在bl31/runtime_svc.c和el3_runtime/aarch64/目录下。关键点在于RT_SVC_DECLARE这个宏。ATF 定义了很多运行时服务比如 PSCI、OP-TEE dispatcher、SPM、SPMD每个服务都通过DECLARE_RT_SVC注册进去形成一个服务描述符数组。当非安全世界的软件执行 SMC #0 指令异常进入 EL3 后runtime_svc.c会根据 SMC 的 function ID 找到对应服务再调用它的 handler。我看源码的时候最喜欢跟一条调用链从 Linux 里执行psci_cpu_on到 EL3 的psci_cpu_onhandler再到真正的 CPU 唤醒实现。这条链把“操作系统发起的请求”和“底层硬件的执行”连在一起比单独看任何一层都有收获。尤其注意psci_affinity_info和psci_cpu_on_finish这两个函数平台移植时你要动的就是它们。1.3 每个 BL 在内存里的最终布局理解内存布局是读懂 ATF 的半条命。BL1 一般放在 SRAM 里的固定地址BL2 根据平台可以放 SRAM 也可以用 DRAMBL31 的 base 地址通常在 platform_def.h 里定义为一个名叫BL31_BASE的宏。BL33 才是我们熟悉的 U-Boot 或 UEFI它的加载地址则取决于 BL2 如何配置 entry point。这里有一个容易被忽略的细节BL31 的代码段、数据段、BSS、页表、执行栈都有固定区域划分用来保证安全世界的意外内存访问不会污染其它区域。看arm-trusted-firmware/include/plat/common/platform_def.h以及具体平台的platform_def.h你能看到一串密密麻麻的#define它们本质上是在给每个阶段划分“宿舍床位”。移植时最忌讳随意改这些地址因为很多值牵一发而动全身。阶段典型运行位置主要职责退出后的去向BL1BootROM/SRAM最小 CPU 初始化验证并加载 BL2跳转 BL2BL2SRAM/DRAM初始化内存与外设加载 BL31/BL33跳转 BL31BL31DRAM 安全区域EL3 runtimePSCI/SMMU/SMC 调度常驻BL33DRAM 非安全区域U-Boot/UEFI/Linux接管系统2. 安全固件工程审计该看哪些代码2.1 信任根与证书链的设计ATF 的安全启动依赖一条 Chain of Trust从固化在芯片里的根密钥ROTKey开始一级一级验证后续镜像。源码里对应目录是drivers/auth/mbedtls/和drivers/auth/它们封装了证书解析、哈希校验、RSA/ECDSA 验签逻辑。真正会看的人会顺着auth_mod.c里的auth_mod_verify走一遍它做的事情是拿到证书或镜像、解析里面的认证参数AuthParam、调用对应的 crypto 模块做验签、检查 Non-Volatile Counter防回滚计数器是否被绕过。如果你在审计一个平台的 ATF 代码第一件事就是确认它的 ROTKey 有没有被硬编码成一个理论上不可信的值或者 BL1 是否在“任何情况下都执行验签”而不是只在某几条路径上验签。有个很朴素但实用的审计思路不是去通读每一行代码而是从攻击面反问自己——“如果我能拿到一个烧录好的固件能不能通过篡改某个镜像让系统跳过验签”看代码的时候带着这个问题效率会高很多。2.2 内存隔离与权限控制在哪里落地ATF 对内存的权限控制主要落在两处第一处是 MMU 页表第二处是 TrustZone Address Space ControllerTZASC。xlat table 库lib/xlat_tables_v2/负责把安全世界的映射设置成“只读”“不可执行”等属性TZASC 则是在总线层面把 DRAM 区域分成安全和非安全分区。审计时重点看两个东西一是页表里有没有把本该只读的区域映射成可写可执行二是 BL31 有没有在运行时主动修改页表属性。ATF 的change_mem_attributes接口确实可以动态改属性但只应由可信代码调用不能被非安全世界通过 SMC 直接操纵。综合来看审计安全固件不能只看 ATF还要看它和 OP-TEE、U-Boot、Linux 之间的交互边界。但 ATF 是根根部烂了上面的安全措施全是隔靴搔痒。2.3 一次固件审计的检查清单我在做实际项目安全评审时会按下面这个列表逐项过你可以直接抄走当参考启动介质上的镜像完整性BL1/BL2 的签名校验是否强制且不可跳过。证书链是否完整ROTKey 是否来自可信源是否允许被替换。Non-Volatile Counter 是否启用防止攻击者回滚到旧版本固件。调试接口是否关闭比如CRASH_REPORTING、LOG_LEVEL在生产固件里要降到最低避免敏感地址泄漏。控制台是否被 GIC 安全组隔离避免安全世界日志被非安全世界直接读取。MMU 属性是否正确关键代码段设为只读关键数据段不可执行。第三方组件版本是否最新mbedtls、OP-TEE dispatcher、SPM 等是否有已知 CVE。FVP 与真机行为差异审计结论必须基于真机或至少是可信模拟环境。3. 平台移植落地指南从零把 ATF 跑起来3.1 移植前先看懂平台抽象层ATF 的代码把“通用逻辑”和“平台相关”分得很开。你移植一个新板子基本不需要动bl31/目录里的通用代码只需要在plat/目录下新建一个平台并实现需要的接口。打开plat/目录你会看到arm/、hisilicon/、mediatek/、qemu/等厂商平台。每个平台下面普遍有这些文件platform_def.h定义内存地址、固件大小、外设基地址。plat_setup.c实现平台初始化包括串口、MMU、GIC、TZASC 等。plat_psci.c实现 PSCI 相关操作。plat_topology.c定义 CPU 簇和核的层级关系。对于新平台我最推荐的方法不是从空文件开始而是找一款架构相近、复杂度适中的平台做模板比如 qemu 或 vexpress然后逐步裁剪替换。因为在 ATF 里平台抽象层虽然接口固定但牵扯到中断控制器、定时器、串口这些细节从零写反而容易漏。3.2 最小可启动平台移植步骤我在一个 Cortex-A72 的双核板子上做过一次最小移植过程大概几步供你参考。第一步创建目录plat/myboard/common/和plat/myboard/include/把platform_def.h写好至少要定义以下内容#define PLATFORM_LINKER_FORMAT elf64-littleaarch64 #define PLATFORM_LINKER_ARCH aarch64 #define BL31_BASE 0x10000000 #define BL31_LIMIT 0x10040000 #define BL32_BASE 0x10100000 #define BL32_LIMIT 0x10180000 #define PLAT_MMAP_ENTRIES 8 #define PLAT_XLAT_TABLES_ENTRIES 16 #define PLATFORM_CORE_COUNT 2 #define PLATFORM_CLUSTER_COUNT 1 #define PLATFORM_MAX_CPUS_PER_CLUSTER 2 #define PLATFORM_STACK_SIZE 0x1000第二步写plat_setup.c里的plat_get_next_bl_params和bl31_plat_setup把串口、GIC、内存映射逐一注册进去。串口最简单的办法是复用 PL011 驱动关键是确认寄存器基地址和时钟频率否则后面你调半天都不会有输出。第三步实现一个最小可用的 PSCI。不需要一开始就把 CPU suspend、system off 全做了先让PSCI_VERSION、CPU_ON、CPU_OFF能通。int psci_second_cpu_on(unsigned long target_cpu, uintptr_t entrypoint) { /* 把 secondary CPU 的唤醒地址写到对应寄存器 */ mmio_write_64((uintptr_t)(CPU_RELEASE_ADDR), entrypoint); sev(); return PSCI_E_SUCCESS; }第四步用支持该平台的交叉编译器构建make PLATmyboard \ DEBUG1 \ LOG_LEVEL50 \ CROSS_COMPILEaarch64-linux-gnu- \ bl31构建出的bl31.bin可以先放到 QEMU 或 FVP 里验证。上板之前先在模拟器里跑通能节省大量反复烧录的时间。3.3 和 U-Boot、OP-TEE、Linux 的联动ATF 不是独立运行的程序它最终要跳转给 BL33。因此在移植完成后还要把 U-Boot 或 UEFI 固件打包成 FIP 镜像或者单独指定 BL33 入口。最直接的做法是# 构建 BL31 make PLATmyboard CROSS_COMPILEaarch64-linux-gnu- bl31 # 构建 U-Boot export BL33/path/to/u-boot.bin # 重新构建并生成 FIP make PLATmyboard CROSS_COMPILEaarch64-linux-gnu- fip如果启用了 OP-TEE则还需要指定 BL32。OP-TEE 的 os 镜像会作为 BL32 被 BL2 加载最终由 BL31 在标准世界和可信世界之间做切换。第一次把这套链路跑通时我印象最深的是明明 BL31 起来了U-Boot 也打印了一堆信息可 Linux 就是起不来。后来发现是 device tree 里没有正确地把 PSCI method 配成smcKernel 根本不知道要通过 SMC 去问 EL3 借 CPU。3.4 平台移植中的三个典型坑第一个是串口打印不出来。极大概率是 console 实例的地址或时钟频率不对。ARM 平台默认用 PL011寄存器基地址错一位、baud rate 算错现象就是完全没有输出。我建议先在 BL31 早期阶段加一个极简单的 GPIO 翻转或点灯确认代码确实执行到那一步再回头查串口。第二个是 BL31 启动后马上 crash。这种崩溃通常会在 console 上打印Unhandled exception和 ESR/ELR 等信息。如果 console 还没初始化那就只能靠 JTAG 或者加个最低级的调试输出。经验之谈先保证 EL3 阶段的 MMU 和异常向量表正确再去谈后面的 PSCI。第三个是 secondary CPU 起不来。这通常不是因为 PSCI 代码错了而是 affinity 层级和硬件实际拓扑对不上。比如你的板子是两簇四核但platform_def.h里写的是单簇双核CPU_ON 就会失败或者唤醒到错误的 core。4. 常见问题与排查技巧实录4.1 问题速查表现象可能原因排查方向完全没有串口输出console 地址/时钟/baud 错误检查 platform_def.h 和控制台驱动BL31 早期 crashMMU、页表或异常向量表问题查看 ESR/ELR缩小到具体指令BL2 找不到 FIPflash 地址或 FIP 格式错误确认 BL2 的 base 与 flash offset验签失败证书链不完整或 ROTKey 不匹配检查 mbedtls 配置和证书文件Linux 不引导PSCI method 没设为 smc检查 device treesecondary CPU 不启动affinity map 与硬件不符核对plat_topology.cU-Boot 启动后内存异常BL33 加载地址超出实际 DRAM重新核对 memory map4.2 调试三板斧第一板斧开日志。构建时设置DEBUG1和LOG_LEVEL50BL31 运行时会输出非常详细的 PSCI、MMU、context 信息。真机上这些日志可能太吵但移植阶段它是救命稻草。第二板斧开崩溃报告。在platform_def.h里确认CRASH_REPORTING打开这样异常发生时能打印出异常等级、ESR、ELR 和 FAR。这几项信息基本能锁定是访问越界、MMU fault 还是指令未对齐。第三板斧固化内存布局图。我会把 ATF 各阶段的加载地址、FIP 里每个镜像的 offset 全部写成一张表调试时对照查。很多莫名其妙的问题其实就是两个镜像放在同一个地址上覆盖了。4.3 一次真实问题排查记录上次调一个四核平台现象是系统偶尔能在 Linux 起来偶尔卡在 U-Boot 阶段毫无规律。第一反应是电源初始化不稳定但查了一圈发现只是 U-Boot 阶段偶尔访问一个mmio地址失败。回头看 ATF 的页表发现那个外设区域的 mapping 属性被配成了 Device-nGnRnE而 Linux 驱动是按 Normal Memory 访问它的结果就是偶发同步异常。改成 Device-nGnRE 之后问题消失。这个案例说明ATF 的 MMU 属性不只影响 EL3 自己还会影响后续阶段对同一段物理内存和外设的访问方式。移植或者集成时一定要和上层的 device tree、驱动对一遍属性别等 Linux 跑飞了才回来查 ATF。5. 移植之后的工程化与长期维护5.1 可复现构建与 CIATF 的构建系统本质上是 Makefile 驱动依赖交叉编译器、mbedtls 子模块、证书生成工具等。为了长期维护我建议把构建容器固定下来明确记录编译器和库的版本。否则半年后团队里再来一个新人很可能因为编译器差异构建出来的 FIP 行为不一致。我在项目里会额外维护一个build.sh把以下动作固化拉取固定版本的工具链、make distclean、生成证书、构建 BL31、生成 FIP。这样无论谁执行产物都能保持一致。5.2 给平台代码留出清晰的“差异边界”平台代码最容易腐化。因为 ATF 通用代码相对稳定平台实现则是跟着硬件改来改去。我个人的习惯是在plat/myboard/下面再分common/、drivers/、include/三级把“真正和板子绑定的代码”和“可以复用的驱动”分开。以后换板子或者升级上游代码diff 起来轻松很多。另外尽量不要在 ATF 的通用目录里直接改东西。如果真的需要改比如要支持某个特殊的外设或修改 PSCI 行为建议把它以 hook 接口的形式加入自己的平台代码而不是去动bl31/里的原生逻辑。这样既方便升级上游版本也减少对安全认证证书链的重新评估工作。5.3 最后分享一个小习惯我个人在实际操作中养成的习惯是每次拿到一块新板子第一件事不是移植完整功能而是先做“最小启动验证”——把 BL31 跑起来串口输出一行版本信息MMU 正常使能然后就停在那里。这一步通过之后再做 PSCI、再做 OP-TEE、再调 U-Boot。这种“层层打通”的节奏看着慢但比一口气把所有功能都铺开再回头找 bug 要快得多。安全固件这种东西出问题的时候不像应用层可以随便打日志它的调试成本高、出错影响大所以把每一步基础打好比任何“优化技巧”都更有价值。