ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ARM Trusted Firmware(ATF)架构解析与安全引导实践指南

ARM Trusted Firmware(ATF)架构解析与安全引导实践指南 1. 先别急着跑代码ATF到底是什么做ARM嵌入式底层开发的人早晚会撞上Arm-Trusted-FirmwareATF这个名字。如果你是从裸机或者只跑Linux内核的层面切入第一次看到“EL3”、“Secure Monitor”、“PSCI”这些术语时大概率会有点懵。网上搜ATF出来的资料要么是官方文档那种标准得让人打瞌睡的叙述要么是零散的移植笔记很少有人把它的架构逻辑、源码组织、安全链路、平台移植放在一起讲清楚。这篇文章是我自己从“能用”到“敢改”再到“敢审计”的一段总结。我不会只贴代码注释也不会通篇翻译文档而是按我实际看代码、改板子、调启动的顺序来走一遍。读完你应该能回答几个关键问题ATF在整个ARM系统里到底站在什么位置BL1、BL2、BL31、BL32、BL33这一串编号分别干什么项目里要把一个新CPU平台支持起来最小改动点在哪里以及从安全工程审计的角度看哪些地方是必须较真的。适合谁看三种人最合适一是做BSP或系统启动开发的工程师马上要接触ATF或已经调得焦头烂额二是做安全评估、固件审计的朋友想从源码层面搞清信任链的构造三是刚入行的嵌入式学习者被各种术语绕晕需要有人用大白话把框架捋直。建议至少有一点ARM64基础比如了解异常级别和TrustZone没有的话也能看懂大半遇到涉及指令集的细节我会给简单解释。2. 全局架构全景从复位向量到操作系统的“接力赛”2.1 异常级别与TrustZoneEL3为什么是“管理员房间”ATF解决的问题根源在ARM架构对安全和非安全世界的硬隔离设计。ARMv8-A把执行环境按异常级别Exception Level从高到低分成了EL3、EL2、EL1、EL0。EL0是普通应用EL1是操作系统内核EL2是虚拟化层EL3是所有级别中权限最高的直接管辖TrustZone的状态切换。如果用一个不太严谨但很好理解的比喻EL3相当于整栋楼的管理员房间不仅要管自家安全世界的设施还能决定楼道里哪些门能开、哪些门不能开。普通操作系统运行在EL1它想请求安全服务比如读取安全存储、校验签名镜像不能直接碰安全硬件只能通过一条特殊的指令SMC陷入EL3由EL3里的Secure Monitor代码代为执行。这就是ATF的核心舞台它常驻EL3既负责引导也负责运行时安全服务。很多新手最初困惑的一点是ATF里的“世界”概念。TrustZone把整个系统逻辑上切成安全世界Secure World和非安全世界Normal World。安全世界可以访问所有物理地址空间非安全世界则要受限于安全地址空间隔离。EL3处于世界切换的枢纽位置任何从Non-secure到Secure的跳转都必然经过EL3的上下文切换。2.2 BL1、BL2、BL31、BL32、BL33五个阶段的职责划分理解ATF最简单的方式是把整个启动过程看成一棒接一棒的接力赛。每个“BL”是某个启动阶段Boot Loader stage的缩写它们的职责边界非常清晰。BL1Boot ROM阶段芯片上电后固化在片上ROM里的代码。这个阶段能做的事情极少因为此时DRAM还没初始化可用的RAM通常只有SRAM或cache-as-RAM。BL1的使命非常简单初始化少量时钟和存储把BL2从Boot Media通常是FIP包中加载到安全RAM里并完成对BL2镜像的认证然后跳转过去。BL1自身不销毁它的代码会在后续阶段继续驻留用于升级或回退路径。BL2可信引导加载器运行在EL1 Secure世界负责更完整的平台初始化主要是内存控制器、串口、定时器等基础外设然后从FIP里加载BL31、BL32可选、BL33通常是U-Boot或UEFI等镜像逐个做签名验证再跳转。它相当于一个“安全检查站”所有后续镜像都由它验证过才放行。BL31EL3运行时固件跳入BL31后CPU进入EL3并常驻在此。BL31不再是临时的“引导代码”而是一个小型运行时系统提供Secure Monitor服务、PSCI电源管理、SMC分发、安全中断处理等。操作系统启动后底层CPU开关、核心上下电、系统重启这些操作都要通过PSCI协议请求BL31来完成。BL32可信操作系统可选项比如OP-TEE、Trusty等。它运行在EL1 Secure世界提供REERich Execution Environment侧请求的加解密、安全存储、指纹等业务。BL33非安全世界引导一般就是U-Boot或者UEFI。它运行在Non-secure EL2或EL1继续初始化Linux需要的环境最终把内核加载起来。这几者的依赖关系是BL1 → BL2 → BL31 →BL32并行→ BL33 → 内核。注意BL31不是“引导完就退役”而是会一直活到系统关机。2.3 FIP镜像与FDT整个启动链路的“物流单”BL1、BL2、BL31、BL32、BL33这些镜像不是散落在存储介质上的而是被打包进了一个叫FIPFirmware Image Package的文件里。FIP里带有一张镜像表BL2解析这张表按顺序读取并认证每一个镜像。FIP的结构可以用一句话概括文件头加上一堆带UUID的镜像块。每个子镜像有唯一的UUID标识比如BL31有固定UUIDU-Boot有另一个UUID。BL2拿到FIP后逐个镜像校验校验通过的加载到指定内存最后按依赖顺序启动。这里有个很实际的问题当你改了U-Boot只需要重新打包FIP而不需要把BL1、BL2全部重新烧一遍。所以实际产品里最常见的部署方式是把BL1放到ROM里不动BL2和FIP放在可升级的存储分区中日常发布只更新FIP。ARM Linux生态里还有一种数据载体叫FDTFlattened Device Tree在ATF里通常作为平台参数传给BL33。ATF在跳转BL33前会把一些平台信息比如电源管理状态、启动原因填充到设备树里U-Boot或内核通过它了解固件层的服务能力。很多人忽略这一点结果移植新板子时发现U-Boot能起来但拿不到正确的唤醒源信息问题往往就出在这。3. 源码工程审计从目录结构读到ATF的设计哲学3.1 顶层目录到底在告诉你什么拿到ATF源码第一件事不是急着找main函数而是先大致扫一遍目录。ATF的代码组织延续了ARM开源项目的风格一眼看过去非常清楚bl1/、bl2/、bl31/各个启动阶段的入口和主流程跨平台代码都在这。plat/平台相关代码这是移植的重点。里面按厂商或开发板划分比如arm/、allwinner/、rockchip/等。drivers/各种外设驱动包括GIC、PL011串口、DP、MTD等按功能模块划分。lib/通用库包括自旋锁、延迟、解析器、崩溃上报等。include/头文件其中include/lib/下有很多架构抽象定义。tools/编译辅助工具比如FIP打包工具、证书生成工具。docs/官方设计文档移植前建议至少阅读plat-porting-guide.rst和firmware-design.rst。一个很容易踩的坑是很多人觉得ATF既然是“固件”应该像MCU固件那样整个工程是一个整体。实际上ATF的编译会分别为BL1、BL2、BL31生成独立的二进制通过tools/fiptool打包到一个FIP镜像中。所以你在Makefile层面可以看到平台只需要配置BL_SOURCES_PLAT、BL2_SOURCES_PLAT、BL31_SOURCES_PLAT这些变量编译系统会自动把各个阶段的源文件组合起来。3.2 启动流程主线从reset_handler到bl31_mainATF的入口不叫main而是reset_handler。因为上电时CPU可能是从BL1、也可能是从直接跳到BL2的复位地址开始的在调试模式或某些仿真场景下可自定义冷/热启动入口。reset_handler里会完成CPU最底层的初始化比如异常向量表设置、MMU配置、栈空间准备这部分代码主要写在bl1/aarch64/bl1_entrypoint.S和bl31/aarch64/bl31_entrypoint.S之类的汇编文件里。理解这段汇编的关键是要分清Cold boot和Warm boot冷启动指整个系统从掉电状态恢复需要重新初始化DRAM、加载镜像热启动指单个CPU核心从低功耗状态唤醒此时大部分系统状态还在。BL1只在冷启动时执行BL31则要同时处理两种场景。BL31的C语言主入口在bl31/bl31_main.c的bl31_main()函数。它的主要工作非常清晰先初始化运行时的服务框架runtime_svc_init、配置PSCI、注册SMC处理函数最后调用bl31_prepare_next_image_entry()准备跳转到BL33。这里的设计很精妙BL31把自身变成一个持续驻留EL3的“微型内核”而把控制权“借”给BL33去跑操作系统。3.3 关键基础设施自旋锁、运行上下文、控制台与内存管理ATF自己有一套并发原语最常见的spinlock在lib/locks/下。因为多核CPU在启动时可能同时进入BL31如果不加锁去操作共享数据结构比如回收队列、CPU状态表很容易出现竞态。别以为固件里面的并发问题少实际上CPU热插拔、suspend/resume时多个核心同时请求PSCI服务是常态锁用不好就会出现莫名其妙挂死。运行上下文cpu_context是另一个核心结构。每次世界切换发生时BL31要把当前CPU的通用寄存器、系统寄存器、栈指针等保存到对应的cpu_context里再恢复目标的上下文。这部分代码在上下文切换性能上极讲究因为Linux的每个系统调用里如果频繁快速切换固件层的开销会直接影响业务响应。控制台console虽然看似不起眼但在调试时是生死攸关的。ATF的控制台驱动设计成了分层console_putc、console_getc、console_flush平台只需注册一个控制台实例。需要注意的是不同阶段BL1、BL2、BL31可能各自要用不同的串口或相同的串口需要在平台代码里正确初始化时钟和引脚否则就是最经典的“完全没输出”问题。内存管理上ATF本身不跑复杂虚拟内存MMU配置也相对静态但需要严格划分“安全DRAM”和“非安全DRAM”。很多平台用TZASCTrustZone Address Space Controller硬件把一部分DRAM标记为安全BL31、BL32的代码和数据就驻留在安全DRAM中普通操作系统永远不能直接访问从而保证固件不被用户态篡改。4. 安全引导链路分析信任是一环一环传递过来的4.1 链式验证从“能跑”到“敢信”ATF默认的安全启动模型是“链式信任”Chain of Trust。每级镜像启动前都要验证下一级镜像的签名。BL1由芯片厂家固化在ROM里它的公钥Hash存储在OTP一次性可编程存储中BL2镜像由BL1用该公钥验签BL2再持有可信的公钥列表去验证BL31、BL32、BL33的签名。这个设计有个很关键的取舍一级的公钥被烧死在OTP出厂后不可改如果未来要更换密钥就需要提供密钥更新机制KUP。ATF的认证框架在drivers/auth/下实现了完整的RSA/ECDSA验签、哈希比对、证书链解析。实际生产环境里通常是建立CA证书体系ATF端只保存根CA公钥每次镜像对应唯一证书。很多工程团队在调试阶段会关闭签名验证比如把TRUSTED_BOARD_BOOT置为0编译出不带验签的版本。这方便但千万不要把这个版本直接发布到量产设备上。我见过某些消费类设备出厂固件就是关掉验证的结果攻击者篡改FIP里的U-Boot就能完全控制设备代价非常大。调试用的版本和发布用的版本应当通过构建配置分开管理最好不要在同一个FIP里混用发布版BL1和调试版BL2。4.2 KEY、证书与打包FIP的生成链路ATF的构建系统沉淀了一整套工具链。通常你会在编译时生成一个_pki目录里面有若干OpenSSL配置文件用它们生成根CA证书、镜像内容证书最后用cert_create工具签名。对初学者来说不用自己从头写证书脚本官方提供的tools/cert_create已经能覆盖大部分需求。实际操作中构建一个带验签FIP的大致流程是生成私钥和证书cert_create -r -o ...等命令生成各级证书。把BL2、BL31、BL33等镜像通过fiptool create放入FIP同时把对应的证书也塞进FIP。把BL1和BL2的bin文件烧到Boot MediaBL2会在启动时自己找到并读取FIP。这里容易踩的一个坑是BL1本身是ROM里固定的BL1验签BL2时使用的公钥不能比BL1里烧录的公钥更靠后。换句话说如果BL1不支持某个算法BL2就不能用那个算法签名。所以在选择密码算法套件时必须考虑芯片BootROM的能力。如果BL1是ARM参考实现通常是支持RSA和ECDSA的但具体哪种曲线、密钥长度要和芯片原厂确认。4.3 安全中断与SMC分发BL31的运行时防护启动完成后BL31最重要的职能就是SMC分发。Linux内核的smc调用会携带一个function IDATF的运行时服务框架根据这个ID分发给对应服务。比如ARM_SIP_SVC、ARM_OPTEE_SVC各服务分别注册自己的handler函数签名基本固定uintptr_t handle_svc(uint32_t smc_fid, u_register_t x1, u_register_t x2, u_register_t x3, u_register_t x4, void *cookie, void *handle, uint64_t flags);关于安全中断要特别留意GIC配置。BL31运行时会把安全中断比如来自安全定时器、安全存储的中断路由到EL3保证即使非安全世界死机了安全世界的中断处理也能正常进行。这一块是工程审计时最容易被忽略的点很多人调好启动就不管GIC了结果系统休眠唤醒时中断路由一团糟。5. 平台移植落地指南把我的代码跑在目标板卡上5.1 移植前先想清楚三个问题平台移植看似复杂核心其实就是让ATF认识你的芯片和开发板。动手写代码之前想清楚三件事你的Boot Media是什么存放FIP的位置和读取方式你的内存格局有多少安全DRAM、多少非安全DRAM你的启动目标是谁BL33是U-Boot还是其他固件。ATF提供了多个参考平台建议从一个相近的参考平台复制出你自己的目录。比如你的SoC和QEMU的ARM版很像就先从plat/arm/board/fvp或者plat/qemu/复制再逐步替换成目标芯片的配置。不要从零开始写平台代码那是纯浪费时间。5.2 搭建最小平台目录每个平台目录至少要包含几个核心文件platform.mk定义平台相关源文件、编译器标志、镜像链接地址。plat_setup.c早期初始化比如串口、定时器、GIC、内存区划分。plat_pm.cPSCI相关回调电源管理。plat_topology.c描述CPU核心列表和电源域用于PSCI和多核启动。include/platform_def.h定义地址常量如安全RAM基地址、UART基地址、中断号。platform_def.h是移植时最重要的头文件。每个宏都有讲究比如PLAT_PHY_ADDR_SPACE_SIZE决定了地址翻译范围MAX_MMAP_REGIONS决定MMU映射表大小PLAT_MAX_PWR_LVL决定PSCI电源域深度设少了之后想支持CPU suspend就会发现接口报错。我在一次移植里吃过亏把PLAT_MAX_PWR_LVL设成了2想支持core、cluster、system三档电源管理时PSCI代码直接断言失败改回3才正常。5.3 最小可启动配置的搭建步骤为了让你看得更明白我把最简启动路径写成一个操作步骤列表。这是一个典型的最小配置不是通用模板但按这个思路去填平台目录一定对在plat/vendor/board/下创建目录例如plat/myvendor/myboard/。从plat/arm/board/fvp/复制platform.mk、plat_setup.c、plat_pm.c等文件先保留FVP的参考实现。修改include/platform_def.h替换成目标芯片的UART、GIC、中断号和内存地址。在platform.mk里把BL31_SOURCES_PLAT指向你的plat_setup、plat_pm等源文件。编译确认至少BL31能打印日志。断点调试在bl31_main()里打印CPU号确认多核启动。接入U-Boot调整BL33加载地址确保ATF跳转后U-Boot能运行。最后做安全特性开Trusted Boot、开安全中断、设置TZASC。每步之间都应该有明确的验证点。千万别把串口、GIC、PSCI、TZASC所有这些改动一次性合入例程里任何一步出问题你都很难定位是哪个环节的锅。5.4 电源管理PSCI回调梳理PSCI是ATF与操作系统之间的电源管理协议。Linux内核在cpuidle、CPU hotplug、suspend/resume时会调用PSCI服务最终执行到BL31里平台注册的plat_psci_ops。该结构体包含cpu_standby、cpu_on、cpu_off、cpu_suspend、system_off、system_reset等回调。回调实现中要特别注意CPU的上下文保存与恢复。以cpu_on为例BL31要把被唤醒CPU的上下文从cpu_context中恢复设置好入口地址再跳过去。很多平台在调suspend时会发现唤醒后系统日志乱掉或者卡在同步原语这通常就是上下文保存不完整比如漏了CNTKCTL_EL1、CONTEXTIDR_EL1这类寄存器导致的。PSCI还有一个容易忽略的affinity info查询功能Linux用它在启动阶段探测CPU拓扑。如果你的plat_topology.c里描述的拓扑与实际硬件不一致比如把cluster数写错那操作系统可能只枚举出一部分核心。5.5 内存布局与固件栈设计把每一块RAM说清楚内存布局是整个ATF移植最容易翻车的地方。ATF自身运行的安全RAM要避开操作系统的物理内存起始区、DMA预留区、以及U-Boot占用区。典型布局是安全RAM放在DRAM的顶部或专用SRAM里BL31的代码段、数据段、栈区都在其中。用platform_def.h里的宏来控制内存大小比如#define PLAT_SECURE_MEM_SIZE 0x100000 #define PLAT_MAX_RAM_SIZE 0x80000000如果安全RAM区域太小BL31镜像会放不下编译时不会报错但加载时可能覆盖其他镜像的数据引发极其诡异的启动报错。我遇到过一个问题BL31里大量使用__attribute__((section(.tzdata)))定义的数据段链接脚本卡得很死一旦超过预留区整个镜像直接加载失败。经验是给自己的安全RAM留足余量比如把BL31镜像大小的两倍作为预留同时开启编译期的section检查。6. 安全固件工程审计不是跑起来就万事大吉6.1 审计清单从信任链到硬件隔离做固件工程审计建议把代码审查、配置审查、形态审查分开推进。下面这个清单是面向ARM固件的常规检查点我能保证这几项在多数项目里都能直接派上用场。信任根与密钥管理BL1固化的公钥是否只存在于OTP是否支持回滚保护。认证算法强度是否用弱哈希密钥长度是否足够RSA-2048以下、SHA-1都要亮红灯。FIP内容完整性是否对BL31/BL33镜像逐个验签是否校验镜像大小和哈希。内存隔离安全RAM区域是否被TZASC正确保护BL31的MMU表是否把非安全外设排除在外。中断路由安全中断响应能否绕过Non-secure世界。热启动路径warm boot入口是否做身份复合校验防止绕过正常启动。调试接口JTAG/SWD是否在生产固件中关闭是否开放UART为root shell。崩溃处理panic后会打印哪些信息是否会把敏感寄存器内容泄露。审计不是简单看一下有没有开Trusted Board Boot还要看实现是否经得起“强制失败”测试。比如故意把FIP里的BL33改坏观察BL2会不会拒绝启动如果BL2在验签失败后仍然尝试执行那这件事就严重了。6.2 常见安全问题ATF项目里发生过的安全隐患很大一部分集中在整数溢出、越界读写、SMC参数校验缺失等。典型例子SMC传入某个长度值固件没有做边界检查就直接在buffer里memcpy攻击者就可能在EL3执行任意代码。一个经典的审计点是SMC处理里的fid高8位识别和宽度校验。许多工程代码里handler对合法范围很宽松导致别名指令被错误分发。建议参考ATF官方对mt、fast、64这些bit的严格判断矩阵逐bit核对而不是简单用if fid 0x82000001这种写法。另一个问题是“悄悄关闭了调试后端”。有些团队在发布固件时才加编译宏把调试串口关掉但代码中仍有在生产版固件中可触发的panic路径打印出的泄露信息包括安全RAM地址、内存映射关系等。审计时最好查看串口使能对应的编译选项确认生产版里console_putc直接被编译掉而不是靠运行时标志位临时关。6.3 我常用的审计辅助手段审计ATF代码只靠人眼不够。我常用的组合拳是用checkstack.pl检查栈压栈深度。用smatch或cppcheck做静态分析重点扫SMC入口和镜像解析代码。用gold linker加--print-memory-usage看内存占用快速定位是否有section膨胀。还有一个简单但高效的测试方法把ARM平台放到QEMU上配合GDB逐步执行BL31的SMC handler喂一个畸形fid观察行为。这种方法可以在不依赖硬件的情况下把异常路径测个七七八八。7. 调试与问题排查遇到重启和挂死别慌7.1 串口完全无输出遇到“烧进去完全没反应”先别怀疑BL1没跑。按这个顺序排查检查串口引脚和波特率ATF参考串口通常默认是115200 8N1但部分开发板拨码或时钟不同。确认BL1的串口驱动初始化是否引用了正确的UART基址和时钟源尤其是外设时钟没使能时寄存器写不进。确认BL1有没有可能跳进错误的分支。看BL1的_start汇编验证复位地址与链接地址是否一致。如果BL1能打印但BL2没反应多半是FIP地址找不到或FIP解析失败。检查plat_get_ns_image_entrypoint和plat_get_bl2_load_info里的地址是否与烧写位置一致。7.2 BL31启动后跳转BL33就挂BL31能把日志打到“Jumping to BL33”但U-Boot起不来这个问题九成出在内存地址冲突。比如BL31把BL33加载的地址和自身使用的安全RAM页表重叠或者ATF传递的设备树地址没有正确传给U-Boot。打开ATF的LOG_LEVEL到LOG_LEVEL_VERBOSE查看最终的mmap映射和跳转参数再对照U-Boot的链接配置基本能定位。还有一个隐蔽点U-Boot对ATF传过来的参数格式有要求比如warm boot入口、D TB地址必须以约定的寄存器传参方式给它。有些平台擅自改了传参形式U-Boot能跑一半就死。此时最好回头确认bl31_params结构里的bl33_ep_info设置。7.3 CPU热插拔或suspend后无法恢复如果启动没问题但在Linux里执行echo 0 /sys/devices/system/cpu/cpuX/online或systemctl suspend后系统卡住优先排查PSCI回调和GIC配置。最常见的问题一个是上下文保存不完整一个是唤醒时中断被屏蔽导致CPU进入WFI后没有中断唤醒它。我建议在地板调试时给PSCI每个回调加一行trace打印把affinity level和state id都打出来。对比正确唤醒和失败唤醒的日志差异基本能看出是CPU_ON没被调用还是被调用后SPR丢失。另外GIC的EOI问题也会导致这个现象安全中断在唤醒时错误地pendingCPU一回来就进中断风暴。7.4 工具链版本和编译环境ATF对工具链版本其实有最低要求但实际中影响最大的往往是GCC版本对bit field的处理方式不同导致某些结构体大小变化。建议固定一个CI镜像里的交叉编译器版本。如果使用了第三方GCC比如带安全加固的编译套件要注意它可能默认开启-fstack-protector-all这会让固件体积明显膨胀最终挤占预留的RAM区域。8. 写在最后一点项目实战体会说一个我自己的经验。早期接触ATF时我习惯把它当成普通裸机工程一看bl31_main就直接找有没有“主循环”结果被各种回调注册、事件驱动、状态机的设计绕得晕头转向。后来我换了个思路把ATF当操作系统内核来看它有自己的任务、锁、内存管理、异常向量表只是形式更小、更静态。从那之后再去看代码理解速度快了非常明显。如果你正准备在真实项目里集成ATF我另外还有几条建议一开始不要开安全启动和TZASC先把基本启动链路拉通拿到一个新平台先在QEMU或FVP仿真板上跑通参考配置给自己建立一个“能正常工作的参照物”平台移植的文档虽然长但plat-porting-guide.rst值得精读一遍很多细节比如plat_log_get_prefix、plat_get_soc_version都是在文档里才能看到的。这块东西内容量不小一篇文章很难面面俱到。但不管是为了快速把boot跑起来还是为了给产品做固件安全评估先把上面的架构脉络梳理清楚心里就有底了。后面的坑咱们在调试日志里再慢慢聊。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进