ATF架构解析与平台移植实战:从源码审计到安全固件落地
发布时间:2026/9/9 7:28:53 锦皓数字建站

ARM 生态下的固件可信根基一直是个既关键又有点“劝退”的话题。很多做应用层或者裸机开发的朋友一听到 Arm Trusted FirmwareATF就头大觉得这东西既涉及安全架构又涉及汇编启动流程门槛太高。但只要你开始接触 Linux 内核启动优化、TEE可信执行环境部署或者正在把 u-boot 从传统 32 位平台往 64 位平台迁移ATF 就是绕不开的一道坎。这篇文章我准备把 ATF 的架构逻辑、源码工程审计方法以及平台移植的完整链路一次讲透不是泛泛而谈概念而是把我在实际项目里趟过的坑、优化过的方案和积累下来的调试心得都摊开来说。无论你是做安全固件评估的还是正在搞国产化平台适配的这篇文章都值得先收藏再细看。1. 内容整体设计与思路拆解1.1 ATF 到底是什么以及为什么你需要关注它Arm Trusted Firmware 这个名字听起来很正式但你完全可以把它理解为 ARM 64 位体系下安全世界Secure World的 Bootloader 和运行时底座。它不是一个单独固件而是一套标准的开源参考实现承担从上电复位到进入操作系统之前所有安全关键阶段的初始化、镜像加载和运行时服务接管。在实际项目中ATF 最常见的存在形式是 BL31Boot Loader stage 3-1它作为 EL3Exception Level 3下的常驻固件在 u-boot 和内核之间扮演“安全管家”的角色。你在启动日志里看到一行NOTICE: BL31: v2.8(debug)之类的内容就是 ATF 在自报家门。很多人以为 ATF 只在开机瞬间起作用这其实是最大的误解——BL31 常驻内存它负责处理系统运行期间的 SMCSecure Monitor Call、PSCI电源状态协调接口、系统 suspend/resume 以及和 OP-TEE 等 TEE 的通信。只要你的系统里还有安全需求ATF 就一直在工作。从工程选型的角度讲ATF 的价值在于它把“安全启动”“运行时异常模型”“可信执行环境接力”这几件最麻烦的事情做成了半成品框架。你不需要从零编写 Secure Monitor 代码而是基于它的框架去适配自己的平台这相当于把 CPU 厂商、平台厂商和系统集成商的工作界面划清了。这种分工方式大大缩短了 BSP 的适配周期也降低了底噪风险。1.2 为什么源码审计和架构理解需要“全景视角”我看过很多人在 ATF 上踩坑根本原因不是代码写不出来而是缺乏全景视角。ATF 的代码量虽然不像 Linux 内核那样庞大但它横跨几个 EL、涉及多种启动介质、包含多套平台抽象层如果在脑子里面没有一张全局图往往会出现“看到一个函数不知道它哪里来、改了一个宏不知道影响哪里”的窘境。理解 ATF 架构的关键在于先接受它的分层逻辑平台层plat→ 驱动层drivers→ 框架层lib→ 运行时服务层services。这种分层很像一套精装修的公寓框架层是水管电线驱动层是开关插座平台层是每个户型的定制化装修方案。源码审计如果只是逐文件往下读很容易被细节淹没但如果你先画出镜像加载链和运行状态机再去对照源码看实现效率会翻倍。1.3 安全固件工程审计到底审什么安全固件审计的重点不是看代码风格也不是单纯找 bug而是看它在攻击面设计和异常路径处理上是否站得住脚。具体来说包括BL33即 u-boot/内核镜像加载时的 PC 指针是否可信、传递给 BL31 的参数是否有校验、SMC 调用号是否被正确过滤、EL3 异常向量表是否会泄漏上下文信息。这些点听起来很基础却是很多商业固件实际出问题的地方。我在做安全评估时有一个固定动作反编译 BL31 的 binary检查异常向量表里 handler 是否对 FIQ、SError 等异常类型做了完整处理。如果发现某个向量的处理逻辑为空或者直接返回这个系统在安全性上就要打个问号。另外一个高危点是调试接口如CONFIG_ARM_GICV3_LEGACY等开关是否在生产固件里被错误打开这比代码逻辑 bug 更致命因为它直接暴露了特权操作入口。2. 核心细节解析与实操要点2.1 四大启动阶段的职责与协作逻辑ATF 官方把启动流程分为 BL1、BL2、BL31 三大固定阶段加上一个可选的 BL32通常是 OP-TEE OS它们各自的定位阶段全称Exception Level核心职能生命周期BL1Boot Loader stage 1EL3安全世界的初始化、加载 BL2完成使命后可回收也可以保留做可信启动根BL2Boot Loader stage 2EL1S-EL1加载 BL31、BL32、BL33 镜像并做校验控制权交接后不再常驻BL31Runtime FirmwareEL3常驻运行时处理 SMC/PSCI接管安全监控系统运行期间一直生效BL32可选TEE OSS-EL1提供富安全应用运行环境系统运行期间可选生效这里要注意BL1 和 BL2 的代码在正常启动完成后往往会被覆盖或置为无效因为它们的使命已经完成。但一些安全要求高的场景下BL1 会保留作为受信任的启动根Root of Trust用来做运行时校验和恢复。我在第一个量产项目里就把 BL1 的镜像区域标记为只读并在 BL31 里做完整性校验这样即便 BL31 被篡改也能第一时间发现。2.2 SMC 调用链与 PSCI 的执行流SMCSecure Monitor Call是普通世界Normal World请求安全服务的唯一入口也是理解 ATF 运行时行为的一条主线。在 x86 世界里你可能用 SYSENTER 或 INT 指令进内核在 ARM64 世界里普通世界调smc #0指令陷入 EL3ATF 的smc_handler跳转到对应运行时服务分支。PSCI 是 SMC 最典型的一种服务。u-boot 或 Linux 内核在 CPU 启动、关核、系统挂起时都会通过 PSCI 接口向 BL31 发请求。比如多核平台上内核并不直接操作硬件寄存器来启停 CPU而是调用psci_cpu_on由 BL31 在 EL3 完成实际的控制。这种设计的精妙之处在于普通世界永远拿不到 EL3 的执行权限任何电源操作都必须经过受信任的固件。即便内核被攻破也无法直接操作什么安全级电源管理功能。对于开发者来说如果调试中发现某个核启动失败第一步就要去查看 BL31 的日志有没有PSCI: Failed to bring up CPU之类的输出然后检查该核的 entry point 地址是否正确以及mailbox核间通信的共享内存里的值是否被错误覆盖。这与我们调试 u-boot 时排查start.S类似思路是共通的。2.3 信任根与镜像校验的设计思路安全固件工程审计的核心之一就是信任链谁才是第一个被信任的代码它又是如何把信任传递给下一级的在 ATF 的设计里BL1 就是信任根通常由 BootROM 加载并利用厂商烧写在 eFuse 里的公钥做签名校验BL1 再校验 BL2BL2 再校验 BL31/BL32/BL33。这样一级一级传递直到普通世界的内核被验证后执行。实现这套机制的关键模块是auth模块。ATF 源码树下的lib/auth目录里实现了基于证书Certificate的验证逻辑支持 RSA、ECDSA 等算法。平台开发者要做的就是把 FIPFirmware Image Package里每个镜像对应的证书和公钥配置好。容易踩坑的点是证书格式不匹配比如用openssl生成的 X.509 证书在 ATF 里解析失败往往是因为 key type 选错了或填充方式不对。我建议直接使用官方docs/getting_started目录下的工具和流程不要自己临时造轮子。2.4 平台移植前的软硬件准备工作真正的移植工作远不是“改改 Makefile 就能编过”这么简单。在动手之前你需要准备好平台 SoC 的内存映射手册尤其是 SRAM、DDR、IO 外设基地址的布局。GIC通用中断控制器版本号是 GICv2 还是 GICv3。这会直接影响中断初始化代码的写法。定时器细节ARM 架构下主要用 Generic Timer但系统计数器基址因厂商而异。官方参考板卡的 ATF 移植代码作为最接近你平台的“参考答案”。串口驱动细节ATF 的 log 输出完全依赖串口很多启动瞬间的 panic 信息都靠它。准备工作本质上是在回答一个问题我要让 BL31 在哪种硬件环境下常驻只要这个问题回答清楚了后续的代码适配就有了边界。3. 实操过程与核心环节实现3.1 获取源码与建立编译环境目前主流使用的 ATF 源码仓库在 GitHub 上TrustedFirmware-A项目建议拉取 LTS 或者接近你平台 BSP 配套的 release tag。我自己习惯用git log --oneline -20看一眼最近的提交状态确认代码不是处于某个中间开发状态。编译环境推荐用 Ubuntu 20.04/22.04 LTS直接安装gcc-aarch64-linux-gnu交叉编译器即可。如果你是 macOS也可以用 Homebrew 装aarch64-elf-gcc但要注意后续一些构建工具的兼容性。命令示例git clone https://github.com/ARM-software/arm-trusted-firmware.git cd arm-trusted-firmware make CROSS_COMPILEaarch64-linux-gnu- PLATqemu DEBUG1 V1这里以 QEMU 平台作为起点是因为它不需要真实硬件也能验证编译流程和启动逻辑。DEBUG1会输出较完整的日志V1则是把编译的每条命令打印出来方便排查头文件路径或宏开关问题。如果编译报错提示找不到某个头文件八成是 platform 的platform.mk里没有包含对应的include路径。比如新平台要用到 GICv3 驱动就需要在平台 mk 文件里加上$(eval $(call add_define,ARM_GIC_V3))这类声明。3.2 平台目录结构与关键文件解析ATF 的每个平台都放在plat/目录下以厂商或 SoC 名称为一层目录比如plat/arm/board/juno、plat/rockchip/rk3399。要移植一个新平台最简单的方式是把相近的参考平台整个拷一份再逐文件替换。平台目录里最核心的文件通常包括platform.mk定义编译选项、源文件列表、链接脚本路径。plat_setup.c实现plat_get_next_bl_params、plat_setup_topology、plat_prepare_bl31等工作。这个文件负责把平台的内存信息、核心信息、电源管理回调等告知 ATF 框架。plat_sip.c或.c实现 ARM SIPSoC-specific SMC调用。很多厂商用它来实现厂商自定义底层操作比如风扇控制、主频查询等。plat_pm.c电源管理回调负责 CPU 上电/下电、系统挂起、系统复位。plat_topology.c描述平台的 CPU、cluster、power domain 层次。include/plat_macros.S platform 相关的汇编宏比如自定义的 crash 打印辅助宏。不要小看plat_setup.c里的plat_get_next_bl_params我在迁移一个 8 核平台时就因为这里的内存节点顺序没配对导致 BL31 给 CPU1 设置的 entry point 地址错乱后续启动直接卡死。这种问题排查起来非常痛苦因为日志并不会明确告诉你“顺序错了”你只能一步步打印 CTRL 寄存器和栈内容去反推。3.3 裁剪和适配以最小启动为例刚开始做平台移植目标不要定得太丰满。先达成一个最小的闭环启动 BL1 → 加载 BL2 → 加载 BL31 → 加载 BL33u-boot→ 看到串口登录提示。为了实现这个闭环你需要搞定以下配置串口基地址和波特率在plat_setup.c里实现console_uart_register。DDR 或者 SRAM 的内存空间描述在platform_def.h里定义PLAT_PHY_ADDR_SPACE_SIZE和PLAT_VIRT_ADDR_SPACE_SIZE。GIC 基地址在platform_def.h里定义PLAT_GICD_BASE和PLAT_GICC_BASE。复位入口在汇编文件里做好 CPU warm reset / cold reset 的分支逻辑。一个完整的配置宏示例如下以 QEMU 为例实际平台按手册替换#define PLAT_QEMU_GIC_BASE 0x2C000000 #define PLAT_QEMU_GICD_BASE PLAT_QEMU_GIC_BASE #define PLAT_QEMU_GICC_BASE (PLAT_QEMU_GIC_BASE 0X10000) #define UART_BASE 0x09000000这些基础定义一旦出错不会有任何高层的提示可能只表现为启动早期死循环或者Synchronous Exception。所以移植初期一定要用示波器或逻辑分析仪抓串口波形别只盯着屏幕看有没有打印——很多情况下打印端口本身就是坏的。3.4 链接脚本与 BL31 内存布局BL31 在系统里的位置是一个常见优化难题。一般情况下BL31 的代码段被加载到 DDR 的某个固定地址但stacks、.bss、page tables这些段往往需要放在安全 SRAM 或 DDR 的保留区域里。ATF 的链接脚本bl31.ld.S里有一套默认布局但平台开发者可以通过platform_def.h里的宏调整。我遇到的典型问题是在开启COHERENT_RAM后BL31 的数据区变得很大超过了预留的内存窗口。解决方式是调整PLAT_RAM_SIZE或者精简驱动模块把不必要的 runtime service 裁掉。每当看到BL31: Failed to get memory日志第一反应就应该是去查链接脚本和平台内存定义而不是去翻业务代码。3.5 FIP 镜像打包与 Boot 流程验证当 BL1/BL2/BL31 都编译通过后需要把它们和 u-bootBL33、OP-TEEBL32可选打成 FIPFirmware Image Package。fiptool就是干这个的位于 ATF 源码的tools/fiptool目录。一个常见的生成命令如下make PLATmyplatform fip \ BL33u-boot.bin \ BL32tee.bin \ BL32_EXTRA1optee_pageable.bin注意不同平台对接 OP-TEE 的参数不一定相同有些平台只传BL32tee.bin有些还要附带BL32_EXTRA1。如果你在启动日志里看到Invalid image type或者Authentication failed先不要急着怀疑安全校验逻辑往往是 FIP 里镜像类型字节对齐或头信息写错了。检查一下fiptool的版本是否和当前 ATF 版本一致——升级仓库后忘掉重建 fiptool 是常见的低级错误但它浪费的时间一点都不低级。4. 常见问题与排查技巧实录4.1 启动早期串口无输出串口没有输出是 ATF 移植第一大痛点。排查顺序建议是检查编译配置里的串口基地址和CONSOLE_DRIVER选择console_16550还是console_pl011。确认 u-boot 的串口配置和 ATF 一致这个“一致”包括地址、波特率、数据位、停止位和流控。看有没有跑飞。在bl31_early_platform_setup2和bl31_plat_arch_setup里加临时 GPIO 翻转或 LED 指示可以快速确认代码执行到了哪里。用 JTAG 调试器直接停在接收到复位向量处。如果这是第一次跑板子JTAG 往往比日志更可靠。4.2 BL31 panic 与 Synchronous Exception启动过程中最慌的莫过于看到一大堆ESR、ELR、FAR信息。其实这些信息足够定位问题ESR的低 26 位暴露了异常类型。0x25这类值通常代表数据异常比如访问了非法地址0x26是指令异常。FAR告诉你触发异常的虚拟地址。对照平台的内存映射表马上能判断是不是访问了不存在的 DDR 区域或外设寄存器。如果在异常向量打印信息里看到SP值特别大或特别小基本可以断定是栈溢出或栈指针未初始化。我处理过一个案例BL31 启动后约 5 秒才触发 SIGSEGV 类似的问题。最后发现是 GIC 初始化时把 SPI 中断的 target 设成了一个UNKNOWN的 CPU 接口编号中断路由后访问了非法寄存器地址。这种问题的根因不在 ATF 框架而在平台的中断矩阵配置日志会把你引导到异常现场但看不出业务逻辑的错误必须回查硬件手册。4.3 PSCI CPU hotplug 失败多核平台上,如果psci_cpu_on一直返回INVALID_PARAMETERS要优先检查两件事plat_core_pos_by_mpidr是否能根据 MPIDR 正确判断亲和层级特别是 big.LITTLE 或大小核架构下cluster ID 和 core ID 的顺序容易混。mailbox里写入的地址是否是缓存一致性区域。如果 mailbox 是普通 DDR而 CPU 从 WFI 唤醒后读到的地址是旧值就会跑到一个无效地址。解决方案是确保 mailbox 地址被标记为MT_DEVICE或共享内存属性并在唤醒路径上做cache clean。这类问题本质上和裸机多核启动遇到的问题高度一致只是披上了 ATF 的 API。把 ATF 看成一个特殊的多核管理固件很多经验都可以平移过来。4.4 安全固件审计的常见暴露点在安全固件审计视角下我一般会重点检查以下几类风险点。它不一定直接向你报错但可能在生产环境里变成实打实的漏洞入口SMC 处理函数对未知命令号的处理安全服务是否对无权限的 Normal World 请求做过滤拿plat_sip_handler来说如果你 switch-case 里漏掉了default分支就可能把未知命令直接透传出去这在攻击者手里是信息泄露的通道。镜像是否强制要求签名哪怕在开发阶段也不要把ENABLE_TRUSTED_BOARD_BOOT0直接挪到量产固件。有些团队做完功能后忘了开启 TBB导致安全链实际上是断的。审计时我会用strings去看 FIP 里有没有证书相关的标记验证这一步是否生效。调试接口是否被关闭比如类SDEI服务、验证后门接口等如果没在构建配置中禁用攻击者可能通过smc #0进入调试模式。量产固件里所有带debug字样的开关都要过一遍。** EL3 堆栈保护**ATF 开启STACK_PROTECTOR的代码路径是否真的覆盖了所有 EL3 入口只保护一部分 handler 等于没做。4.5 常见问题排查速查表现象可能原因优先排查点启动早期无打印串口配置错误、基地址不对console 基地址、驱动选择BL31 卡在plat_setup之后内存映射表缺失或异常platform_def.h内存区间PSCI CPU1 无法上线mailbox 地址问题、缓存未同步缓存维护与地址映射属性镜像校验失败TBB 开启后证书不匹配证书链的pubkey是否需要提前烧录升级 ATF 后性能下降缓存属性或 GIC 优先级配置被默认覆盖检查PLAT_PARTITION_BLOCK_LIST相关宏5. 平台移植落地的进阶建议5.1 一套可以参考的移植排期平台移植这件事最怕做一步看一步没有节奏感。以我自己的经验一个中等复杂度的 N 核 SoC从零移植 ATF 到跑通 Linux通常可以按下面时间节点安排第 1~2 周吃透参考平台的初始化流程对照 SoC 手册整理出内存、外设、中断、定时器的地址表并实现最小 BL31 编译和 QEMU 模拟跑通。第 3~4 周在真实硬件上跑通串口、GIC、定时器完成多核启动和 PSCI 流程。第 5~6 周接入 OP-TEE如果产品需要验证安全世界与普通世界的切换、SMC 通信。第 7~8 周开启 Trusted Board Boot、关闭调试接口做安全固件自查整理文档和收尾。这个排期看起来很长但其实每个阶段都会有大量排查和迭代时间。如果压缩到两三周往往意味着安全验证是不够充分的量产之后再来补课成本更高。5.2 善用多套“参考实现”做比对大部分 SoC 的参考平台不是凭空来的它们都参考了 ARM 官方 FVP 平台或者其他开源项目。移植的时候不仅看你手头 SoC 厂商的代码也把plat/arm或者类似架构的第三方平台源码下载下来对比。很多不好理解的设计决策在对比之后会豁然开朗。比如 GICv3 初始化时GICD_CTLR的写入顺序我在官方平台和某第三方平台里看过两套处理顺序虽然结果一样但注释解释了不同 SoC 上的时序坑。这种经验是厂商文档里很难给你讲清楚的。5.3 长期维护中的工程化实践ATF 是一个持续更新的项目你在项目里锁定一个版本可以但一定要关注上游对安全漏洞的修复和更新。建议做这几件事记录本地 patches 基线所有非上游代码单独放在plat/下面不要改动上游核心文件。每次升级 ATF 版本后跑一遍完整的安全启动自动测试覆盖 SMC 模糊输入、异常中断风暴、反复 suspend/resume 等场景。在 CI 里加入 FIP 生成步骤确保任何代码变更都会产生一个可验证的固件镜像而不是等发版前再手动操作。我自己吃过亏的地方是改动了一个 GIC 配置宏结果 BL32OP-TEE的中断路由乱了出现偶发性死机。如果没有自动化测试这个问题可能在生产环境里潜伏很久。越早把这些工程化流程建起来后期越安心。5.4 把 ATF 安全能力往业务侧延伸ATF 的价值不只在于启动它还能为整个系统的可信执行环境提供很重要的能力。比如通过 SMC 向普通世界提供安全存储、密钥存取能力让业务软件能校验自己的完整性。利用 EL3 的ARM_GIC能力快速处理安全中断避免 SecurCore 被打断。和 OP-TEE 或 TrustZone 结合把指纹、支付等敏感业务隔离在安全世界里执行。做过商业安全产品的朋友都清楚真正的安全不是某一个模块的防守而是各层可信机制的连贯与协作。ATF 是这条链条的底层锚点它立住了上层安全才有处可依。回到文章开头那个困惑——ATF 入门难不难我的答案是如果你把它想象成一块需要“可信接力”的接力棒一级一级往下传即便中间有掉棒异常你也能通过日志、硬件跟踪和逻辑推演把它接回来。希望这篇实操向的源码评测和移植指南能帮你把 ATF 这块硬骨头啃得更顺一点。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。