
ARM平台的安全启动链上Arm Trusted FirmwareATF是绕不开的一段。只要芯片贴着AArch64基本就要把它当成EL3下的开机固件和运行时服务框架来用。ATF核心干两件事一是把系统从冷复位一路带到Normal世界的Bootloader二是长期驻留在EL3世界向Linux/RTOS提供PSCI、SDEI这类安全服务。这篇文章我会从架构全景、安全固件审计视角、平台移植落地三个维度把ATF的源码结构和实操路径拆开讲适合正在做平台bringup、TrustZone相关功能开发或者准备把Linux/OP-TEE接到自家SoC上的工程师。1. 项目概述ATF到底在ARM体系里扮演什么角色1.1 先给一个清晰的总体定位Arm Trusted Firmware现在官方仓库叫TF-ATrusted Firmware-A是Arm提供的开源参考固件运行在EL3异常级别是整个ARMv8/AArch64启动链里安全世界最初的那一段代码。它干的主要事情包括初始化最小系统环境、建立安全世界的内存视图、加载并验证下一级镜像、提供电源管理和安全服务给Normal世界的软件调用。所以你在做ARM平台Bringup时ATF就是你在RESET之后第一个跑的、有完整工程结构的软件。它不仅要能在你的平台上跑起来还要能正确地把控制权交到U-Boot或EDK2手里过程里还不能破坏Normal世界的后续启动。很多工程师会把ATF和Bootloader混为一谈实际不是一回事。ATF的边界很明确它是EL3世界的基础固件U-Boot这类Bootloader通常运行在EL2或EL1属于Normal世界的软件。ATF的作用是“启动它们”而不是“替代它们”。1.2 标题里的三个关键词对应什么工作“架构全景”说的是ATF内部那套BL阶段划分、运行时服务注册机制、镜像加载与安全启动链路“安全固件工程审计”说的是用安全视角去把源码过一遍找出可能被攻击或配置不当的点“平台移植落地”则是把一个新板子从零接到ATF上包括新建platform目录、实现板级回调、打包fip、和U-Boot/OP-TEE接驳。这三件事其实是三个不同层次的技能需求。架构全景偏源码阅读和逻辑拆解工程审计需要你带着攻击面思维去看设计平台移植则更偏工程实操靠的是对回调函数和构建流程的熟悉。1.3 适合谁看看完能解决什么问题如果你正在调一个冷启动就挂的板子或者准备给厂商SDK里的老旧ATF升个级又或者想让设备支持Trusted Board Boot这篇文章能给你一个相对完整的路线图。我尽量把源码里容易让人绕晕的地方用比较直白的方式讲清楚也把那些“文档里不会写”的坑单独拉出来说。2. 源码架构全景ATF的骨架与核心组件2.1 从复位到交棒的启动链路ATF把启动过程拆成四个正式阶段外加一个可选的Secure-EL1 Payload阶段分别是BL1固化在ROM里的第一阶段引导代码负责冷复位后的最小初始化加载并验证BL2。BL2运行在安全世界DRAM/SRAM里的第二阶段引导代码负责加载BL31、BL32若有、BL33并完成安全启动验证。BL31EL3运行时固件核心是PSCI、SDEI、Runtime Services长期驻留。BL32可选的安全世界OS最常见的是OP-TEE。BL33Normal世界的可信Bootloader或其他镜像通常是U-Boot/EDK2。启动顺序如图逻辑所示ROM → BL1 → BL2 → BL31 → BL33BL32则根据平台设计由BL2加载、BL31带它进Secure world。每条链路的职责边界其实很清晰。BL1是最小、最安全的启动代码它不信任任何外部镜像连BL2都要经过验证才加载。BL2是一个“中转站”它把后续镜像从非易失存储读到内存并处理签名认证。BL31是真正意义上的“常驻固件”系统启动完成后BL31不会退出它留在EL3里等待Normal世界通过SMC指令来调用服务。这里有一个很重要但容易被忽略的点如果你使用的是RESET_TO_BL311的路径那BL1和BL2是会被跳过的ROM或SPL会直接把BL31加载到EL3。这种路径常见于某些SoC把ROM里的固件自己写好、只用ATF的EL3运行时能力。移植前必须想清楚走哪条路径因为BL2是否参与决定了安全启动验证链路怎么搭。2.2 运行时服务PSCI、SDEI与SPMBL31启动完成后会进入一个等待SMC请求的循环。Normal世界通过smc指令陷入EL3BL31拿到调用号后分发给对应的Runtime Service。其中最重要的小弟是PSCIPower State Coordination Interface它定义了CPU hotplug、suspend/resume、系统重启/关机这些操作的标准接口。ATF里的services/std_svc/psci/就是PSCI实现的核心目录。板级支援包里一大堆plat_psci_ops回调就是给PSCI用的。SDEISoftware Deferred Exception Interface是给Normal世界提供异步事件通知的机制适合做RAS错误上报、虚拟化场景的中断转发。它和PSCI的调用路径不同SDEI有自己的事件注册和注入流程。SPMSecure Partition Manager则是为了支持安全分区到了新版本里更多提FF-A。如果你打算跑丰富的安全世界负载这块要单独关注。从源码阅读的角度我建议先读PSCI因为它和平台移植关系最紧密。SDEI和SPM可以先放一放等主线逻辑通了再回头看不迟。2.3 安全启动与镜像打包TBB / FIPATF把多个镜像打包成一个FIPFirmware Image Package镜像。FIP不是压缩包它是一个按特定格式拼接的容器由fiptool创建。构建ATF时通过fiptool create将BL2、BL31、BL33等镜像组合在一起统一烧入Flash。安全启动TBBTrusted Board Boot的核心是信任根。BL1里烧录了一个RoT公钥BL1用这个公钥验证BL2的签名证书BL2又再用证书链去验证BL31、BL32、BL33的签名。整个验证链的根不依赖外部存储因此攻击者即使能改Flash内容也无法绕过镜像验证。打包命令大致是这样make PLATqemu DEBUG1 CROSS_COMPILEaarch64-linux-gnu- fiptool fiptool create --tb-fw build/qemu/debug/bl2.bin \ --soc-fw build/qemu/debug/bl31.bin \ --nt-fw u-boot.bin \ fip.bin加载BL2时BL1从FIP里读出BL2镜像并验证BL2随后读取剩余镜像并继续验证。目前很多消费级平台即使不上TBB也会把FIP当做一个标准镜像格式来维护方便烧录和升级。建议新平台从一开始就把FIP的格式定下来别等后期再补。2.4 代码地图目录结构与“值得读”清单ATF源码目录结构本身就是一个很好的学习材料。顶层目录大概如下bl1/、bl2/、bl31/各BL阶段入口和主流程。plat/平台相关代码按厂商和板卡组织。lib/通用库包括el3_runtime、xlat_tables、psci、utils等。drivers/GIC、串口、定时器、SCMI等外设驱动。include/头文件导出公共接口。services/运行时服务实现包括std_svc、spd、sdei。tools/fiptool、cert_create、sptool等工具。精读建议先从bl31/main.c和bl31/bl31_main.c入手看BL31初始化流程然后读PSCI的状态机和psci_ops之后再读plat/arm/board/下的某个真实平台代码比如Juno或QEMU理解板级回调怎么实现。平台代码里最需要关注的文件是plat_board.c、plat_board_topology.c、plat_board_helpers.S和platform.mk。这四个文件基本决定了一个板子能否被ATF认得出来、跑得起来。3. 安全固件工程审计以“挑刺”视角过一遍ATF3.1 信任根与镜像验证链路审计做固件安全审计首先看信任根。ATF里默认的TBB配置里ARM_ROTPK_LOCATION可以指向devel_rsa或devel_ec256这类开发用密钥。开发时用这些密钥没问题但生产固件如果忘了换成自己的RoT私钥那和没开安全启动没有区别。审计时需要检查你的平台是否还在用仓库自带的测试证书build/platform/release/certs/里生成的是不是开发密钥ROT_KEY是不是指向自己的密钥文件另外要看镜像验证是否覆盖了所有关键镜像。有些平台只在BL1验证BL2、在BL2验证BL31但BL33完全没验证这样Secure Boot等于开了一半。特别是产品里跑第三方Linux镜像时BL33应当是验证链的末端不能漏。3.2 内存隔离、MMU与TrustZone配置检查ATF所有世界都跑在同一个物理地址空间里安全世界与Normal世界靠内存访问权限和TrustZone控制器来隔离。ATF会用页表把Secure RAM标记为Secure世界独享Normal世界访问会被阻塞。审计时重点看plat_board_def.h里的内存映射定义以及plat_board_common.c里调用的mmap_add_region参数。如果Secure RAM被映射成Non-Secure属性或者页表属性里漏掉MT_SECURE那Normal世界的Linux活脱脱能读到安全世界的敏感数据。TZASCTrustZone Address Space Controller也需要人工核对。不同SoC的TZASC配置方式不一样有些是ATF里配有些是U-Boot里配。无论在哪配都要确保安全DRAM区域的region分割正确Normal世界的地址访问会被硬件挡在外面。这是审计里比较基础、但又最容易出问题的地方。3.3 SMC入口与EL3暴露面检查ATF的EL3入口是Normal世界可以直接触达的攻击面。攻击者只要在Normal世界执行一条smc指令就能把CPU带入EL3。因此SMC handler的参数校验、权限校验、世界归属判断都非常重要。在源码里services/std_svc/std_svc_setup.c负责标准服务调用分发SPD相关代码里也有自己的SMC入口。审计时注意几个点handler有没有检查调用方世界secure/normal有没有检查参数长度和指针合法性有没有把安全世界的敏感信息直接回传给Normal世界常见风险之一是某个服务函数只判断了smc_fid没判断参数合法性结果攻击者传入异常地址导致EL3崩溃或者读取了EL3内存。因此凡是进入EL3的接口都应该默认所有参数是不可信的。3.4 加固选项与常见弱点清单ATF编译时提供不少安全加固选项强烈建议生产构建全部打开ENABLE_PAN禁止EL3在访问普通内存时用EL3特权绕过PSTATE.PAN。ENABLE_UAO禁止EL3使用非特权访问覆盖。ENABLE_PAUTH打开Pointer Authentication防止ROP攻击更有效。ENABLE_BTI打开Branch Target Identification。ENABLE_AMU打开活动监控单元虽然更多是功能特性。ENABLE_STACK_PROTECTOR启用栈保护。如果平台不支持这些特性至少把ENABLE_PAN和ENABLE_STACK_PROTECTOR打开。这两个对通用攻击的防御效果最直接。还需要审查编译选项里有没有关闭.rodata只读段的重新映射以及BL31运行时栈是否处于可写可执行区域。如果是那一旦栈被溢出攻击者就能直接执行PWN代码ATF的栈默认应该是NX但要确认没被平台自己的linker script给破坏了。可以把常见问题汇总成表审计项检查点风险等级信任根密钥是否使用默认测试密钥高BL33验证BL2是否验证BL33签名高Secure RAM映射页表是否标记MT_SECURE高TZASC配置安全region是否被Normal世界覆盖高SMC参数检查handler是否验证参数合法性中读保护安全日志/证书是否对外泄露中编译加固PAN/UAO/Stack Protector是否开启中4. 平台移植落地指南手把手加一个新平台4.1 移植前的清单与准备把一块新板子接到ATF上第一步不是写代码而是收集硬件资料。你需要至少搞清楚以下信息SoC型号、CPU核心数、cluster拓扑。冷复位后CPU在哪个异常级别执行。系统内存地址空间、DRAM与SRAM的起始地址和大小。串口型号PL011、16550或其他及基地址。GIC版本GICv2/v3/v4及分发器、CPU接口基地址。是否有TZASC/TZPC等TrustZone控制器地址段怎么配置。你想要用什么Bootloader接棒和它的内存布局约定。工具链方面官方支持GCC、Clang、Arm Compiler。最常见的是aarch64-none-elf-或aarch64-linux-gnu-交叉编译工具链。这里说一句题外话如果你还在找Arm Compiler 5.06那类老编译器ATF的AArch64路径基本用不上它优先选择较新版本的GCC或AC6省很多编译兼容问题。建议将基线放在近期稳定的release分支不要直接跟master。ATF仓库活跃度高master上经常有API调整和未验证的平台改动跟随master会放大Bringup难度。4.2 创建一个最小平台目录与构建脚本ATF的平台代码组织方式非常固定新建平台只需按这个模板走。假设我们要添加一个名为myboard的平台属于虚构的myvendor厂商plat/myvendor/myboard/ ├── platform.mk ├── include/platform_def.h ├── plat_myboard.c ├── plat_myboard_helpers.S ├── plat_myboard_topology.c ├── plat_myboard_pm.c └── plat_myboard_trusted_boot.cplatform.mk里定义该平台参与编译的源文件和头文件路径PLAT_BL_COMMON_SOURCES \ plat/myvendor/myboard/plat_myboard_helpers.S \ plat/myvendor/myboard/plat_myboard.c BL2_SOURCES \ plat/myvendor/myboard/plat_myboard_trusted_boot.c BL31_SOURCES \ plat/myvendor/myboard/plat_myboard_topology.c \ plat/myvendor/myboard/plat_myboard_pm.cplatform_def.h定义硬编码的地址、基地址和内存size#define PLAT_DRAM_BASE 0x80000000 #define PLAT_DRAM_SIZE 0x40000000 #define PLAT_MAX_PWR_LVL 2 #define PLATFORM_CORE_COUNT 4 #define PLATFORM_CLUSTER_COUNT 1plat_myboard.c里实现最基本的配置函数例如时钟、串口、内存映射void plat_myboard_setup(void) { /* 初始化串口、GIC、系统控制器等 */ console_init(...); /* 配置内存映射 */ mmap_add_region(PLAT_DRAM_BASE, PLAT_DRAM_BASE, PLAT_DRAM_SIZE, MT_MEMORY | MT_RW | MT_NS); }一个最小平台能通过编译后先别急着堆功能把BL31跑到能打印日志、能跳转BL33就行。4.3 实现PSCI、GIC等板级函数PSCI不能没有没有PSCI的话Linux连CPU hotplug、CPU idle、reboot都做不了。你需要实现plat_psci_ops里最核心的几项cpu_standby、pwr_domain_on、pwr_domain_off、system_off、system_reset。以CPU_ON为例板级代码需要告诉PSCI怎样把某个核心从断电状态拉起来。通常流程是写电源管理寄存器等核心上电等待MMU和缓存配置完成返回成功。伪代码如下static int myboard_pwr_domain_on(u_register_t mpidr) { unsigned long cpu_id mpidr MPIDR_AFFINITY_MASK; /* 设置核心上电触发寄存器 */ mmio_write_32(MYBOARD_PWRCTL_BASE 0x10, cpu_id); return PSCI_E_SUCCESS; } static const struct plat_psci_ops myboard_psci_ops { .pwr_domain_on myboard_pwr_domain_on, .system_reset myboard_system_reset, .system_off myboard_system_off, }; int plat_setup_psci_ops(uintptr_t sec_world_id, const struct plat_psci_ops **psci_ops) { *psci_ops myboard_psci_ops; return 0; }GIC初始化通常放在BL31的早期启动里保证中断控制器工作正常。GICv3相对简单后续的interrupt routing、虚拟化中断支持要按平台需要增加。定时器一般用系统级别的Generic Timer不用额外驱动。但如果你要支持suspend/resume或者需要给OP-TEE提供时间服务需要在BL31里把CNTFRQ_EL0设置成正确的频率。4.4 打包FIP并与U-Boot / OP-TEE接驳编译平台make PLATmyboard DEBUG1 CROSS_COMPILEaarch64-linux-gnu- \ BL33u-boot.bin all fipBUILD会把bl31.bin、bl31.elf、fip.bin生成出来。fip.bin里已经包含了u-boot.bin可以直接烧到启动介质里。U-Boot和ATF的接驳有两种常见方式BL1 → BL2 → BL31 → U-BootATF负责加载U-BootU-Boot作为BL33。SPL → BL31 → U-Boot如果SoC的ROM直接引导SPLSPL再加载BL31ATF的BL1/BL2被跳过U-Boot作为BL33由BL31接棒。第二种方式在消费级SoC里非常常见因为ROM固件厂商自己写好了ATF只需要提供BL31。编译时需要设置RESET_TO_BL311并且SPL要能传递必要的信息给BL31。接OP-TEE时BL31需要选择对应的SPD。OPTEE的SPD实现位于services/spd/opteed/BL31初始化时会通过SPD注册接口找到OP-TEE。编译时需要在BL31链接中带上opteedOP-TEE镜像本身作为BL32打包进FIP。4.5 用日志与调试器验证移植ATF跑起来的第一件事就是看串口日志。编译时设置LOG_LEVEL40能看到较详细的VLOG/INFO如果LOG_LEVEL50则是所有日志。make PLATmyboard DEBUG1 CROSS_COMPILEaarch64-linux-gnu- \ LOG_LEVEL50 RESET_TO_BL311 all日志里如果出现了类似NOTICE: BL31: v2.10(release):和INFO: plat_myboard_setup这类输出说明BL31已经通过板级初始化。如果跳转BL33时卡住可以通过BL31打印的next image