
做嵌入式系统开发这么多年我越来越觉得有一类固件很像“房间里的大象”——平时大家讨论U-Boot、讨论内核、讨论rootfs但真正要把一台带TrustZone的ARMv8/Armv9 SoC老老实实跑起来把安全启动、可信执行环境这些特性打开你绕不开一个项目Arm-Trusted-FirmwareATF工程圈更习惯叫它Trusted Firmware-ATF-A。这篇内容我打算把它从整体架构、源码工程、安全审计思路到平台移植的完整落地路径一次性说透。这篇适合谁看呢两类人最该读一类是刚接手带安全特性的ARM平台、U-Boot能跑但一开Secure Boot或OP-TEE就卡住的同学另一类是已经用过ATF但只是“点一下编译脚本”的开发者我保证这篇文章里有一些代码实现细节能让你对BL层之间到底做了什么有更直观的理解。文章不会有那些照抄文档的废话都是我实际读源码、实际改平台、实际踩完坑之后的心得。1. 先把“全景”看清楚——ATF到底管什么事1.1 从异常级别模型说起EL3为什么这么特殊很多新手第一次接触ATF时第一反应是“这不就是一段启动代码吗”然后顺手把它和U-Boot SPL放一起理解结果越看越乱。我建议你先把处理器特权模型捋清楚。在AArch64里系统有四个异常级别EL0是用户态EL1是操作系统内核EL2是虚拟化层HypervisorEL3是实现安全监控Secure Monitor的最高特权层。ATF的核心职责就是在EL3这一层提供一套安全固件参考实现。它既负责启动早期的可信引导流程也负责系统运行阶段承接来自非安全世界的安全服务请求SMC调用、电源管理PSCI、安全中断路由等。也就是说ATF不是一段“跑完就消失”的启动代码它里面有一个长期驻留在EL3的运行时固件BL31只要系统不关机它就在那待命。很多人容易忽略一点EL3拥有访问“安全世界”和“非安全世界”所有资源的权限。这既是ARM TrustZone安全扩展的关键也是攻击者最眼馋的入口。一旦EL3固件里有漏洞非安全世界的普通内核代码就可能借机拿到整个系统的最高权限。这也是为什么ATF这类固件在工程审计时要比普通Bootloader严格得多。1.2 启动链路拆解BL0、BL1、BL2、BL31、BL32、BL33ATF的启动流程本质上是一条“逐级验证、逐级交接”的信任链官方叫BLBoot Loader阶段BL1通常固化在芯片ROM里是复位后执行的第一个程序。它体积很小负责初始化最小系统环境然后从Flash里读取并验证BL2。BL2运行在SRAM中是可信启动的核心。它负责加载下一阶段所有镜像BL31、BL32、BL33并完成证书校验、镜像鉴权。BL31EL3运行时固件。它启动后不退出而是驻留EL3提供runtime services然后跳转到BL33。BL32可选的可信操作系统TEE OS比如OP-TEE、Trusty。它通过BL31里对应的SPDSecure Payload Dispatcher和普通世界通信。BL33非可信固件一般是U-Boot、UEFI或者其它引导程序最后进入主操作系统。我习惯用一个不太严谨但很好记的类比BL1是保险柜的第一道机械锁任何人都撬不动BL2是门禁保安只放行有合法工牌签名证书的人BL31是常驻管家管理房子里所有特权事务BL33是正式访客通过管家的许可才能入场。这也是ATF和传统“单级Bootloader”最本质的区别它把“启动”拆成多个可信层级每级都依赖上一级完成验证而不是一次性把所有代码搬到内存里就跑。分段的意义在于ROM里的BL1可以做到几乎不可篡改信任根Root of Trust天然隔离BL2和BL31的尺寸、存储位置、运行空间可以做独立优化。1.3 内存布局与TrustZone分区数据放哪里很关键看ATF源码时还要有“内存视角”。每个BL阶段的二进制都是链接到固定物理地址上的这些地址通过平台头文件里的宏定义指定。常见的内存区域包括ROM区域放BL1随机存储启动时使用。SRAM区域放BL2和BL31这部分内存也常被称作Trusted SRAM要求它在安全世界控制下。DRAM区域存放BL32TEE以及BL33等镜像。安全固件工程里一个核心原则是“非安全世界可访问的内存固件默认不可信”。所以ATF在启动过程中会通过MMU把外设、内存映射成“设备内存”或“普通内存”并设置不同的权限属性。特别是有DMA能力的外设它的缓冲区如果放在非安全内存里就要格外小心——这在后面审计章节会详细讲。2. 源码工程细读——代码怎么组织、安全机制怎么落地2.1 仓库结构与模块划分ATF源码拿到手后第一件事不是急着搜索函数而是先看目录结构。顶层目录大体可以按功能分成几类bl1、bl2、bl31各启动阶段的入口、主流程、上下文管理。common平台无关的通用逻辑比如描述镜像的descriptor、加载流程、buffer管理。lib底层库包括el3_runtime异常级别切换、上下文切换、psci电源管理、xlat_tablesMMU页表、utils延迟、随机数之类。plat平台代码这是移植时改动最多的目录。drivers各种驱动比如GIC、串口、定时器、存储控制器。fdts设备树源文件用于fconf动态配置。tools证书生成、固件打包等工具。我建议阅读顺序是先读bl31/main.c和bl31/runtime_svc.c理解runtime services框架再读common/desc_image_load.c理解镜像加载流程最后再深入lib/el3_runtime和lib/psci。只要这两条主线通了再零散的驱动代码都能慢慢填进去。2.2 Runtime Services框架与SMC分发机制BL31里真正精巧的设计是runtime services框架。它把EL3需要提供的所有服务归成一张“服务表”每个服务有自己唯一的SMC调用ID区间比如PSCI、SPD、SIP、OST。当普通世界或安全世界发起一条SMC异常时BL31会先保存现场、切换到EL3然后根据调用ID找到对应服务执行对应handler处理完后恢复现场并返回。核心代码在bl31/runtime_svc.c里两个最重要的数组是rt_svc_descs和rt_svc_descs_indices。每个服务描述符里有init函数和各类handler函数指针。这种设计的好处是解耦ARM自家可以加PSCIOP-TEE可以加OPTEED芯片厂商可以加SIP服务互不影响。移植时如果要在ATF里加一条私有SMC指令本质就是定义一个新的runtime service。但这也意味着每一类服务的handler都是攻击者的潜在入口。ATF的SMC入口代码smc_entry、smc_handler做了很多检查比如CPU是否处于安全状态、是否来自被允许的异常级别、调用ID是否合法。读代码时一定要关注这些前置条件判断安全固件的“第一道防线”就在这里。2.3 可信启动与测量启动ATF的Trusted Board BootTBBR是整个安全工程里最值得研究的部分。开启TBBR后BL2会使用X.509证书链逐级校验镜像签名。信任链的起点是一组烧录在芯片中的RoT公钥Trusted Root Key流程大致是BL1验证BL2镜像BL2的证书由平台私钥签名。BL2加载并验证BL31、BL32、BL33的证书与镜像。为每一段镜像计算摘要Hash并记录到事件日志中供上层系统度量启动结果。在较新版本的TF-A里还引入了测量启动Measured Boot和Event Log机制可以把启动过程中所有度量值记录下来交给TEE或由Rich OS读取作为远程证明Remote Attestation的数据源。如果公司做的是金融支付、车机、服务器一类的产品这个机制基本是标配要求。2.4 源码评测后的几句大实话从代码工程角度看TF-A整体抽象得不错模块边界清晰尤其runtime services框架的设计经得起推敲。但历史包袱也相当明显编译期的宏开关数量堪称“杂草丛生”同一个功能在不同平台上有三四套实现路径老平台的代码维护成本很高很多坑是从旧代码一路保留下来的。新特性比如fconf动态配置、RMM支持Arm CCA确实在往好的方向走但仍需要时间把历史债务清干净。所以读这套源码心态要好遇到看不懂的宏开关不要硬啃先看它要解决什么问题再看平台有没有打开它。3. 平台移植落地实操——从一个最小平台开始3.1 移植之前的准备工作我强烈不建议一上来就扑向真实SoC。除非你手里有芯片原厂全套的参考代码否则连调试串口都点不亮的时候你会分不清到底是硬件问题、工具链问题还是ATF问题。最佳路径是先用QEMU或FVPFixed Virtual Platform把编译和启动流程跑通再对照自己板子的内存映射、外设基址去改代码。QEMU最大的好处是免费且容易拿到GIC和串口模型都是现成的很多真实平台甚至可以直接复用它的驱动再改改地址。工具链方面ATF官方推荐的交叉编译工具链是GCCaarch64-none-elf-gcc 或 aarch64-linux-gnu-gccARM编译器armclang 6.x这里特别提醒一句网上很多朋友还在找ARM Compiler 5.06u7下载但新版TF-A已经不支持老旧的armcc 5了官方文档明确推荐GCC 7.3以上或armclang 6.x。如果你还在用5.x版本升级工具链往往是解决编译期“莫名其妙报错”的最快路径。3.2 最小平台需要实现的接口清单在plat目录下新建自己的平台目录后除了platform_def.h外有几类接口是必须实现的。我把常用的列出并说明用途接口/文件主要作用备注platform_def.h定义地址、大小、宏开关移植最先改的文件bl31_early_platform_setup2()早期初始化串口、计时器、内存信息没它连日志都看不到bl31_plat_arch_setup()建立MMU页表配置内存权限权限配错会导致启动异常plat_get_next_bl_params()返回下一个阶段BL32/BL33入口参数内核跳转错就看这里plat_get_syscnt_freq2()系统计数器频率影响PSCI的suspend/resume计时plat_core_pos_by_mpidr()MPIDR到逻辑CPU编号的映射多核启动依赖它plat_setup_topology()描述CPU簇拓扑涉及PSCI的层级电源管理GIC驱动实现配置中断控制器安全/非安全路由中断功能绕不开这些接口不一定每个都写在同一个文件里可以根据平台习惯拆成platform_common.c、plat_setup.c、plat_pm.c、plat_gic.c等。参考已有的QEMU或FVP实现是最省力的方案。3.3 从零创建平台的步骤演示以我常用的方式为例我会这样搭一个最小平台假设叫myboard第一步创建目录结构plat/myboard/myboard/ ├── platform.mk ├── platform_def.h ├── plat_common.c ├── plat_setup.c ├── plat_pm.c ├── plat_gic.c └── aarch64/ └── plat_helpers.S第二步在platform.mk里指定源码文件和依赖include plat/arm/common/arm_common.mk PLAT_BL31_SOURCES \ plat/myboard/myboard/plat_common.c \ plat/myboard/myboard/plat_setup.c \ plat/myboard/myboard/plat_pm.c \ plat/myboard/myboard/plat_gic.c第三步编写platform_def.h至少把DRAM基址、UART基址、GIC基址、BL31运行地址定义出来。以QEMU板为参照类似#define DRAM_BASE 0x40000000 #define DRAM_SIZE 0x80000000 #define ARM_GICD_BASE 0x08000000 #define ARM_GICR_BASE 0x080A0000 #define PLAT_UART_BASE 0x09000000 #define BL31_BASE 0x04000000 #define BL31_LIMIT 0x04020000第四步实现串口初始化。如果是ARM标准PL011可以直接复用drivers/arm/pl011/pl011_console.c里的驱动在bl31_early_platform_setup2里调用console_pl011_register。第五步实现GIC初始化。以GICv3为例需要配置GICD/GICR的基址并在bl31_plat_arch_setup里把这两个外设区域映射为设备内存。GIC初始化顺序错了往往表现为主核能启动但中断一上来就panic。第六步编译验证make CROSS_COMPILEaarch64-none-elf- PLATmyboard DEBUG1 V1 bl31编译完会生成build/myboard/debug/bl31/bl31.elf和相关bin文件。用QEMU跑起来看到类似下面的日志说明基本通路已经打通NOTICE: BL31: v2.10(release):myboard NOTICE: BL31: Built : ... INFO: BL31: Platform setup start INFO: BL31: Platform setup done到这里一套最小平台就已经立起来了。后续再根据真实SoC去替换地址、增加驱动、开启TBBR。3.4 平台移植过程中的几个“暗坑”移植时最容易被忽略的三件事我逐一提醒。第一MMU页表属性。很多新手只翻译地址不设置内存属性。结果调试串口输出乱码外设被当成普通内存缓存了或者BL31跑飞。外设区域一定要用设备内存属性Device-nGnRnE普通RAM才用Normal属性并配置合适的Cache策略。第二栈空间与内存重叠。BL31的栈顶和堆空间通常由链接脚本控制如果BL31_BASE和BL31_LIMIT范围开得不对栈可能会踩到BL32镜像加载地址表现为系统运行一段时间后随机崩溃。第三系统计数器频率。PSCI的suspend/resume、CPU off/on都依赖系统计数器。如果你在平台里没有正确实现plat_get_syscnt_freq2()或者返回的频率和GIC的计时不一致会看到电源管理相关调用表现为“睡下去就醒不过来”。4. 安全固件工程审计——源码里该盯紧哪些位置4.1 先从攻击面分析入手ATF的EL3固件从攻击面角度看主要有三类输入SMC调用运行在非安全EL1/EL2的代码内核、虚拟机可以主动发起的入口。中断安全中断如TEE的定时器、安全外设中断会打断非安全世界强制切入EL3。启动镜像BL2加载的所有镜像如果签名验证不严格攻击者可能通过替换镜像控制启动链。做安全审计时我习惯先看SMC处理路径。一条SMC从异常触发到业务handler中间要经过保存现场、解析调用ID、查找服务、权限检查、执行处理、恢复现场这么多环节任何一环出了纰漏都可能被利用。重点检查点包括handler有没有对入参做范围和合法性校验有没有对来自“非安全世界”的地址做安全问题判断有没有直接信任调用者传入的长度导致缓冲区溢出或者越界读写。4.2 可信边界的保持内存与外设权限固件最忌“过度信任”。在ATF语境里TrustZone的地址空间控制器通常会在启动早期就把DRAM分成安全区域和非安全区域。比如OP-TEE使用的内存必须标记成安全区域非安全世界的DMA不能访问。而ATF自身运行时用到的堆栈、数据段必须放在非安全世界不能篡改的内存中。如果平台配置有误让BL31的某些数据结构落在非安全内存里攻击者就可以通过U-Boot或内核修改这些数据后续诱导EL3固件做错误决策。审计时建议全局搜索那些带NSNon-Secure标志的内存地址逐一确认它们的用途是否有被篡改的风险。4.3 已知漏洞类型的通用排查思路虽然我不建议生搬硬套CVE编号去审代码但历史修复记录里反复出现的漏洞类型非常值得关注整数溢出在处理镜像大小、缓冲区长度的乘法或加法时如果长度是攻击者可控的溢出后可能绕过边界检查。未初始化的栈数据异常处理路径上如果上下文保存不完整可能把敏感数据留在栈里高权限固件再把它返回给低权限调用者。释放后使用或双重释放ATF早期大量使用静态内存较少动态释放但与TEE交互的共享内存管理偶尔会出现这类问题。缓存一致性问题共享内存shmem比较危险一个CPU写入数据另一个CPU因为缓存未回写读到旧值极端情况下可能绕过安全状态检查。我自己的习惯是拿到一个新版ATF源码先看它相比上一版在security相关函数里新增了哪些校验再回到旧代码里对比差异。很多安全修复的diff就能看出来设计者对“威胁模型”的思考方式这种“比较式阅读”对提升工程审计能力非常有帮助。4.4 可信启动的端到端验证最后审计别忘了把可信启动链路从头到尾验证一遍关掉TBBR能启动打开TBBR后把BL33镜像篡改任意一个字节理论上系统必须在引导早期就被拦下来。如果还能继续启动说明证书校验或镜像鉴权的流程没有被正确执行。这类问题通常是证书生成工具链与固件配置里的哈希算法不一致导致的排查优先级极高。5. 常见问题与排查技巧实录5.1 典型问题速查表现象最可能原因排查方向编译报错没有规则可制作目标平台目录或platform.mk里路径写错检查依赖文件是否有拼写错误编译报undefined reference漏加了源文件或宏没定义对照PLAT_BL31_SOURCES列表串口无任何输出串口驱动没注册或时钟没初始化检查bl31_early_platform_setup2与UART基址输出乱码MMU把UART区域映射成了Normal Cache外设必须映射成Device内存卡在BL31跳转BL33BL33加载地址或入口参数不对检查plat_get_next_bl_params内核起来后中断全乱GIC初始化或安全中断路由错误对照GICD/GICR基址和配置流程CPU off/on失败PSCI相关hook或拓扑信息有误检查plat_core_pos_by_mpidr与topology打开TBBR后启动失败证书链生成工具或算法不一致检查证书签名算法与固件内部摘要算法5.2 串口日志的调试技巧ATF各个BL阶段都有独立的日志开关编译时通过LOG_LEVEL控制冗余程度0是ERROR10是WARNING20是NOTICE30是INFO40是VERBOSE。调试阶段建议直接LOG_LEVEL40这样能看见平台初始化、MMU映射、镜像加载等大量细节。如果串口一个字符都没有先别急着怀疑驱动。直接把LOG_LEVEL调到40在bl31_early_platform_setup2入口处加一个临时输出确认代码是否执行到了这里。如果连这里都没到问题基本在BL1/RSE阶段或者BL2跳转BL31时地址不对。如果到了但没输出再查UART基址和引脚复用。5.3 用调试器和内存转储辅助定位ATF可以配合JTAG或OpenOCD调试把BL31的elf文件加载进调试器里直接在bl31_main、runtime_svc_init这些函数下断点。这种方法对定位“哪一步导致panic”尤其有效因为ATF支持打印当前ELR、SPSR和栈回溯在PSCI调用异常时能快速定位到是哪个CPU、哪一条指令出了问题。平台代码里建议保留一个panic时的快速诊断函数把当前的mpidr、寄存器快照、调用栈指针全部打印出来。实际产品中这些信息对分析“概率性崩溃”的价值非常大。5.4 后续还可以怎么扩展如果你已经能把自己的平台稳定跑起来那么下一步自然是接OP-TEE开启Secure Boot甚至尝试Arm CCA相关的RMM。接OP-TEE的核心是在BL31里把opteed这个SPD编译进去并在BL2加载BL32镜像让BL31通过opteed_init拉起TEE OS。整个过程比ATF最小平台再多出几步但有了前面的基础你会更容易理解各阶段镜像之间的参数传递和上下文切换逻辑。最后再分享一个实操体会我最初读ATF源码时犯过一个典型的错误按文件顺序从头读到尾结果读了两周还是糊的。后来换了个思路先不看具体平台把BL1、BL2、BL31的主流程和runtime services框架拎出来再用QEMU跑一遍最小系统整个脉络一下就通了。如果你也要做平台移植我的建议是先跑通参考平台再改自己的硬件参数千万别急着把每个宏都搞清楚。等你的板子能稳定打印出BL31 banner、能正常跳转U-Boot再回头看源码很多设计意图你自然就懂了。ATF这套固件的确有历史包袱但作为ARM生态里规模最大、应用最广的EL3参考实现读懂它带来的收益远超你花掉的这些时间。