
很多人第一次翻开 Arm Trusted FirmwareATF现在也叫 TF-A源码时都会产生一个错觉觉得它不过就是个带签名的 bootloader使命就是验证下一级镜像然后跳转。等真正把一个新 SoC 的移植任务接过来或者试图从安全角度对这份固件做工程审计时才会意识到 ATF 远不止于此。它实际上运行在 ARMv8/AArch64 的 EL3 异常级别里是整条安全启动链路的起点与信任锚点也是运行时电源管理、安全服务、可信 OS 调度的常驻载体。这篇文章不是泛泛的概念梳理而是基于源码逐模块拆解、以及在多个平台上的移植和问题排查经验把 ATF 的架构全景、安全边界、平台落地路径一次讲透。适合读这篇文章的人正在做 ARM 平台 BSP/固件开发的工程师准备把 ATF 移植到新 SoC 或新板卡的人以及做嵌入式安全审计、对固件攻击面负责的安全工程师。如果你只是想大概知道 ATF 是干嘛的也能从启动链和异常级别这部分带走一个清晰的框架。1. 点亮一块板子之前ATF在启动链里扛住了什么1.1 AArch64的四个异常级别决定了ATF必须存在AArch64 把 CPU 的运行特权划分成 EL0 到 EL3 四个异常级别。EL0 跑普通应用EL1 跑操作系统内核EL2 是虚拟化层EL3 则是最高特权级整个系统的安全和信任决策最终都得落到这里。ARM 的设计里EL3 属于 Secure World 的根基而 ATF 就是常规情况下唯一运行在 EL3 的固件。为什么要单独留一个 EL3 出来因为现代系统里安全和功能必须解耦。普通 OS 可能被攻破Hypervisor 也可能出 bug可一旦这些组件被攻破后还能直接控制电源、篡改安全启动流程、读取可信 OS 的敏感数据那整个系统的安全模型就崩塌了。EL3 的意义在于把决定性操作收编到一块无法被普通软件触碰的内存和异常级别里外部只能通过约定的接口主要是 SMC 指令请求 EL3 代为执行。ATF 就是这套机制的软件实现是 EL3 世界里那个什么都管但尽量少暴露的看门角色。从这里能顺带理解一个经常被问的问题为什么不能只用 U-Boot或者干脆把启动代码写在裸机里原因很直接——启动只是一瞬间的事运行才是常态。系统起来之后还需要安全世界配合处理电源状态转换、安全中断、可信 OS 调度。只有 ATF 这类常驻 EL3 的固件才能持续提供服务。你把它当 bootloader 看格局就小了。1.2 从BL1到BL33一条完整的启动链契约ATF 把启动过程拆成了 BL1、BL2、BL31、BL32、BL33 几个阶段每个阶段都有严格职责边界。阶段运行位置职责体积/生命周期BL1通常烧在 ROM 或一次性写入区域最小编译单元负责加载并验证 BL2上电即跑启动完成后基本不再对外服务BL2Trusted SRAM 或 DRAM 安全区域可信引导固件加载 BL31/BL32/BL33完成镜像签名/证书校验只在启动阶段活跃BL31EL3 Runtime常驻运行时固件提供 PSCI、SDEI、SPM 等服务U-Boot 和内核运行期间一直存活BL32Trusted OS可选项比如 OP-TEE 这类可信 OS与 BL31 配合服务安全世界BL33非安全世界引导普通 OS比如 U-Boot 或直接 EFI 启动正常系统引导入口这套设计的深意在于最小可信计算基BL1 是最先执行的代码所以必须做得极小、极简单所有可能被攻击的逻辑都尽量往后放。BL2 虽然做大量镜像加载和校验工作但它在 BL1 的验证保护下运行且完成使命后其占用的内存可以被复用或保护。BL31 则是唯一常驻的 EL3 代码它的健壮性直接决定运行时安全所以独立成块与启动逻辑物理隔离。搭建信任链时ATF 遵循 Trusted Board BootTBBR 思路芯片厂商预置 Root Of Trust通常是一把烧在 eFuse 里的公钥哈希BL1 校验 BL2 的证书BL2 再校验 BL31/BL32/BL33 的镜像哈希和证书链。这样每一级都建立在前一级可信基础上从物理根一直延伸到操作系统加载。1.3 EL3 runtime不是跑完就死ATF的常驻属性很多人把固件理解为一次性代码其实 BL31 加载完成后永远不会退出。CPU 在 EL1 跑 Linux遇到 PSCI 电源管理调用、安全中断、或者来自 Secure World 的请求时都会通过 SMC 指令陷入 EL3由 BL31 处理完再返回。这个模型跟微内核 系统调用很像只不过这里的用户态是 Linux/Android内核是 EL3 的 runtime 服务。这也是为什么 ATF 源码里会同时存在中断处理、SMC 分发、内存权限管理、上下文切换这些机制。它得像一个微型操作系统那样管理自己的世界。理解了常驻属性再去看代码里那些为什么要单独给 BL31 配链接脚本为什么有这么多人写各自平台的电源域描述之类的问题思路就会顺很多。2. 源码目录就是一张活地图按执行流拆ATF2.1 bl1、bl2、bl31三段式执行流强隔离不是灵光一现ATF 源码顶层目录第一眼就暗示了架构bl1、bl2、bl31 三个平级目录恰好对应三个阶段。每一层都尽量不引用其他层的业务代码数据交换靠显式传递的结构体或固定内存地址。这种强隔离的好处是审计时能快速锚定每一段代码的信任边界不需要在层与层之间猜测隐式契约。具体到每一层BL1 目录很小核心逻辑集中在早期初始化、BL2 镜像加载与验证。代码里你会看到大量对 MMU 还没开启时的物理地址操作的判断因为它运行在最原始的环境连 DRAM 都未必初始化完。BL2 目录负责真正的装配工角色它把 BL31/BL32/BL33 的镜像分别加载到约定地址并对每个镜像做哈希/签名校验。它的日志里到处是 Loading image、Authentication 之类字样调试启动问题先看它。BL31 目录则是运行时固件的入口要初始化 EL3 异常向量表、初始化各类 runtime service、建立 Secure/Non-Secure 世界的中断路由然后切回非安全世界把控制权交给 BL33。此后 BL31 就靠异常向量表和 SMC 分发逻辑持续存活。阅读源码时我推荐按执行流来读而不是按目录名通读。先在上电点打好断点从 bl1_entrypoint 一路往后跟再回到各个服务的注册与调用路径会清晰得多。2.2 services 与 libATF真正藏功夫的地方很多人只盯着 bl1/bl2/bl31 这三个目录忽略了 services、lib、drivers 这几个目录才是区分能不能落地的关键。services 目录里放着所有 EL3 运行时服务的实现PSCI 电源管理状态机、SDEI 事件通知、SPMD 安全分区管理、TRNG 随机数服务等。每个服务通过一个统一的注册宏比如 DECLARE_RT_SVC 这类机制把自己挂到 runtime service 框架上SMC 指令进来后分发器根据服务 ID 找到对应 handler。lib 目录则更底层包括内存缓冲管理、异常处理、栈保护、缓存管理、同步原语等。读这些代码时可以特别关注两个点一是所有跨世界数据访问是否都做了边界检查二是是否存在可能被非安全世界诱导的缓存一致性问题。这两个点也是安全审计的高频入口。drivers 目录不仅是串口这类基础驱动还包括 GIC通用中断控制器、TZC-400、CCI/CCN 一致性互连、认证密码模块等。平台移植时驱动层往往决定了工作量的上限——一个 SoC 即使核心逻辑照抄参考板外设初始化路径不同照样要在驱动层花大力气适配。2.3 一条SMC指令从用户态到EL3的完整旅行为了直观理解 ATF 运行时模型可以追踪一次典型的 PSCI CPU_SUSPEND 调用。用户态内核执行 SMC 指令后CPU 立即陷入 EL3硬件自动跳转到 BL31 在启动时安装的异常向量表。同步异常入口捕获这次触发后首先保存非安全世界的寄存器上下文然后进入 SMC 分发逻辑。分发逻辑做的事情很像路由器解析 SMC 功能 ID根据 fid 的位段确定这个请求是标准服务、快速服务、还是安全分区相关服务再查表路由给具体 handler。PSCI 的 CPU_SUSPEND handler 拿到目标 CPU 和电源状态调用平台提供的电源操作函数完成实际的下电/挂起流程最后在醒来时恢复上下文回到内核。整个过程看似简单但每一步都涉及安全态与非安全态的上下文隔离以及内存中 Secure/Non-Secure 属性的切换。这也是为什么 ATF 的代码里到处是上下文结构体的保存与恢复。3. 安全审计视角读ATF源码不是通读是找边界3.1 攻击面盘点先清楚什么会被外界碰到做固件安全审计第一步不是看算法、看密码强度而是盘点攻击面。ATF 的攻击面可以从下面这个表快速建立框架。攻击面入口典型风险SMC 接口非安全世界通过 SMC 指令发请求handler 对参数校验不严、服务 ID 误路由加载阶段镜像BL1/BL2 读取并校验 BL2/BL31/BL33哈希/证书绕过、整数溢出导致越界读调试接口JTAG、串口控制台调试端口未禁用、打印泄露内存信息电源管理接口PSCI 状态转换状态机错乱、非安全世界触发未授权电源操作中断路由GIC 配置安全中断被重定向到非安全世界Secure Partition / 可信OS通过 SPM/SPMD 交互世界切换时的上下文污染这个攻击面模型并不神秘核心思路是从非安全世界能触达的每个入口出发沿着数据流走向 EL3 内部看是否有越权、越界、状态绕过、类型混淆的可能。审计日志和 dump 往往就是从这些入口抓到的第一手证据。3.2 ATF源码里的防御设计哪些值得当成模板优秀的安全固件不是只做功能还会在源码层面留下对抗攻击的痕迹。ATF 里有几个典型的防御设计跟进代码时值得留意。栈保护编译时开启 stack protector栈金丝雀受随机种子影响能有效阻止简单的栈溢出利用。内存权限管理MMU 页面属性里明确区分 Secure/Non-Secure、RO/RW、可执行/不可执行。审计时要重点检查是否存在把 Non-Secure 可写内存标记为 Secure 可执行的配置。输入边界检查所有 SMC handler 对长度、偏移、指针的校验是否覆盖了全部入口是否缺少负数/超大值检查。镜像认证BL2 对镜像做多级认证证书链审计时要关注认证失败后的错误路径是否干净有没有认证失败但继续执行的旁路。中断隔离安全中断必须被路由到 EL3/安全世界非安全中断不能直接打断安全世界的敏感操作。GIC 配置错误往往是审计发现的高频项。这些设计不是孤立存在的。如果平台移植者在 platform_def.h 里把某段内存错误地映射成了可写可执行那么 ATF 内置的所有防御都会在这一处失效。所以审计时不要只看 ATF 自身代码还要连同平台的链接脚本、MMU 映射表一起过一遍。3.3 我实际审计时会走的思考路径我的习惯是先建立信任边界 输入入口矩阵再针对每个入口做数据流追踪。比如 SMC 接口我会先从 services 目录列出全部已注册 service ID再逐一审查 handler 对 SMC 参数的引用路径。对我这种一次一行的读法最有效的代码辅助工具是配合文档阅读重点看那些被注释为assumption的地方——这类隐式前提往往是分析的突破口。另一个常被忽略的审计角度是未使用路径。比如某些平台配置了调试 UART 但在正式打包时没有禁用调试打印或者在 FVP 参考板上能用的 JTAG 调试功能在产品板上没有关闭。代码审计不只盯已实现的功能更要盯不该存在却存在的接口。4. 平台移植落地从拿到参考板到跑起自己的SoC4.1 移植前先回答三个问题比写代码重要第一你的 SoC 用哪块内存当可信内存ATF 的 BL1/BL2 和 BL31 各自需要一块固定的、非安全世界不可访问的地址空间。如果直接用 DDR要确保 DDR 控制器初始化由哪级代码完成ATF 各阶段能否在 DDR 未初始化时运行如果 SoC 有内部 SRAM则要确认容量是否装得下 BL31。第二串口和时钟树是什么情况启动调试几乎完全依赖串口先确定 ATF 使用的 UART 是 PL011 兼容还是 16550 兼容、基址是多少、时钟频率是多少。好多移植卡在没日志上的根因不是逻辑错误而是串口基址写错或时钟算错。第三电源状态模型长什么样PSCI 需要知道这个 SoC 支持哪些电源域每个电源域有哪几个状态每个状态对应什么硬件操作。参考板 FVP 的平台描述可以复制但实际 SoC 的 power controller 操作完全不同。这三个问题各对应一个主要工作量内存映射修改、驱动适配、PSCI 平台操作实现。如果你的 SoC 在这三方面有现成参考移植就能控制在几天内如果都要从零探索那最好把时间预算放宽到两周以上。4.2 移植要动的核心文件逐个说明它的角色ATF 的平台相关代码一般放在 plat/ / 目录。从参考板复制一份出来后主要修改集中在几个文件platform_def.h定义内存地址、支持的电源层级数、核心数、堆栈大小等。几乎每个宏都可能影响编译和运行改的时候务必逐个确认。platform.mk声明 BL2/BL31 编译时需要加入哪些源文件链接器脚本怎么选。漏加一个源文件会让某个 API 变成未定义符号或者功能缺失。plat_setup.c负责 MMU 初始化、平台内存映射表、BL31/BL2 的平台相关初始化函数bl31_plat_setup、bl2_platform_setup。plat_topology.c描述电源域树PSCI 依赖它来判断 CPU 的上线下电顺序。plat_psci.c实现 PSCI 的平台相关操作电源状态转换、系统复位、系统关闭。链接脚本bl 各阶段的 .ld.S决定各镜像的内存布局和符号位置常见翻车点是把堆栈段放在不该放的区域。从我自己的经验来讲我移植时第一步永远是先让串口能在最早期打印一段固定字符确认时钟、电源、UART 三个基本条件成立第二步才是扩展平台初始化逻辑直到 Linux 启动到用户态。没有串口日志的调试等价于蒙着眼睛修 bug千万不要跳过这一步。4.3 内存映射移植中最容易翻车的地方ATF 运行时MMU 必须把所有可能访问的地址段建立映射并标注好属性和权限。映射表通常写在 plat_setup.c 里一行一个 region。以常见写法为例static const mmap_region_t plat_mmap[] { MAP_REGION_FLAT(DEVICE0_BASE, DEVICE0_SIZE, MT_DEVICE | MT_RW | MT_SECURE), MAP_REGION_FLAT(DRAM_SECURE_BASE, DRAM_SECURE_SIZE, MT_MEMORY | MT_RW | MT_SECURE), MAP_REGION_FLAT(DRAM_NS_BASE, DRAM_NS_SIZE, MT_MEMORY | MT_RW | MT_NS), {0} };这里最容易出的问题有三个。属性错配把外设寄存器映射成了 MT_MEMORYCPU 对外设的访问会被缓存扰乱出现灵异读写。地址越界region 的 base size 超出地址空间宽度导致映射重叠或覆盖其他安全区域。权限过宽有些内核镜像区域既不需要 Secure 也不需要可写开发时图省事直接 MT_RW | MT_SECURE上线后就成了攻击者可利用的内存漏洞。我建议移植时先把所有地址整理成一张总表跟 datasheet 的地址映射一列一列核对完毕再动映射代码。这一步多花半小时后面能少熬一个通宵。4.4 拉通一次编译和启动的完整流程假设你的 platform.mk 已经按需填写编译这步反而最简单make PLATyour-platform DEBUG1 \ ARM_ARCH_MAJOR8 \ BL33u-boot.bin路径 \ allDEBUG1 会开启较多日志和一些断言移植阶段强烈建议打开。BL33 传 U-Boot 镜像的位置ATF 在 BL2 阶段会自动把它打包进 FIP。生成文件里主要关注 fip.bin 和 bl31.bin前者是完整启动镜像后者可以单独烧写调试。烧写后串口能看到类似 BL1: Booting BL2 的日志然后依次是 BL2: Loading image、BL31 初始化、跳到 BL33。如果卡在某一步优先看它前一段日志里是否出现 Authentication failure 或 Data abort。前者通常指向证书/哈希问题要在 make 命令里关闭或配置认证后者多与内存映射或地址访问越界有关。把 U-Boot 跑起来之后再用启动 ARM Linux 验证 PSCI 的 CPU 上下电、reset 和 poweroff。如果 Linux 能启动但无法进入深度睡眠或关不掉核问题大概率在 plat_psci.c 的电源操作实现而不是启动链。5. 移植与调试中翻车概率最高的几个点5.1 卡在最早期的BL1/BL2不出日志先确认终止点是真的没有开始运行还是运行了但串口没输出。我遇到过的案例里后者占比更高。检查顺序UART 基址是否和 datasheet 一致、时钟树是否初始化、波特率是否设置正确、控制台驱动是否被 BL2_BL31 等源文件编译进去了。如果这些都没问题再回到内存映射里确认串口地址被覆盖为 MT_DEVICE否则 MMU 开启后串口操作会被缓存出现有打印但乱码或延迟的诡异现象。5.2 BL2往BL31跳转时挂掉这个位置出现问题十有八九是 BL31 镜像被加载到了不安全或不可执行的内存区域。先在链接脚本和 platform_def.h 里确认 BL31_BASE 落在安全可执行区域再用反汇编工具确认 BL31 第一条指令确实在该地址。另一个常见原因是 FIP 里 BL31 镜像格式或哈希不匹配导致 BL2 校验失败如果临时关闭认证再把证书配置路径对上就能快速定位。5.3 PSCI 相关操作无效或被内核拒绝Linux 启动后如果 CPU hotplug 或 suspend/resume 频繁失败先不要急着怀疑内核。在 EL3 侧打印 PSCI 回调是否被触发然后确认 plat_get_power_domain_tree_desc 里层级描述是否和实际硬件一致。很多 SoC 的 cluster 层级在移植时被简化成单层导致内核下发的电源状态 ID 映射错乱这种问题往往只靠读日志很难一眼看出来。5.4 调试手段从浅到深的完整链路我的推荐顺序是先打开 DEBUG1 编译看 EL3 日志然后利用 ARM Trace 或 JTAG 连接在关键函数入口如 bl31_main、runtime_svc_init打硬件断点。逻辑分析仪/示波器用于确认电源控制引脚的物理时序这是软件日志完全无能为力的场景。如果平台支持 FVPFixed Virtual Platform模拟器非常建议先在 FVP 上跑通参考配置再对照硬件调试。FVP 的好处是能稳定复现问题避免硬件抖动干扰判断。一个实战经验遇到启动问题不要一味加日志先判断当前事件发生在 MMU 开启前还是开启后、在 Secure 世界还是 Non-Secure 世界再用对应的手段物理断点、trace、异常向量去截获。盲目打日志往往把一个偶发问题改成加了日志就消失了反而更难排查。到这里ATF 从架构全景、源码执行流、安全审计视角到平台移植落地的完整路径已经拉通。最后分享我个人在这件事上最大的体会读 ATF 源码不要试图一次性背下所有 API而是把握住EL3 世界 执行流 数据边界三个主轴做移植时先让日志亮起来再逐步增加复杂度做安全审计时永远问自己哪些路径是攻击者能触达的、哪些状态是我不希望攻击者进入的。把这几个习惯建立起来ATF 对你来说就不再是一团黑盒固件而是一套可以掌控、可以审计、可以按需改造成自己平台一部分的坚实基础。