ATF固件架构与安全启动链路深度解析:从BL1到BL33的移植实践 📅 发布时间:2026/9/9 7:19:21 👁 浏览次数: 在嵌入式底层这个圈子里聊到系统安全与启动链路Arm Trusted FirmwareATF是一个绕不开的名字。无论你是做手机SoC的BSP还是搞服务器、网关、工控板卡的固件开发只要芯片是Arm架构且要跑Linux或标准生态大概率会跟ATF打交道。很多人手里有它的源码也知道它是“跑在EL3的那段固件”但真要回答 BL31 在整个系统里扮演什么角色、BL2 为什么要有两层、平台移植时到底要动哪些文件这些问题能讲清楚的人并不多。这篇文章我从源码阅读的角度切入结合我在实际板卡上把ATF从无到有跑起来的经验把它的架构全景、安全工程审计要点和平台移植路径拆开揉碎讲一遍。内容偏底层、偏固件适合有一定嵌入式基础、正在啃ATF源码或准备做平台适配的开发者。我会尽量说人话把复杂的地方掰开讲。1. 为什么Arm要单独搞一个ATF以及它到底解决了什么问题1.1 从“裸机启动”到“安全启动”的演进逻辑在早期ARMv7甚至更早的时代SoC上电后一般只有一个BootROM它从存储介质里把u-boot或裸机程序拉起来然后直接把控制权交给OS。整个链路里没有“安全世界”的概念TrustZone虽然已经存在但它更多是用在运行时隔离上启动阶段几乎是裸奔的。任何人只要能改写启动介质里的内核镜像就能完全控制设备。现在越来越多的设备暴露在公开网络环境中启动链路上如果没有可信根后面做再多安全防护都像在沙地上盖楼。ARMv8架构引入EL3Exception Level 3之后硬件上终于有了一个独立的、比EL2/EL1更高的特权层。ARMv9继续沿用了这套分级设计并在RMERealm Management Extension里进一步扩展了EL2的粒度。ATF就是专门跑在EL3和Secure EL1/S-EL2这一层的可信固件。它最核心的价值是把“硬件可信根”固化成一条软件链路上电后由内嵌在SoC里的BootROM作为信任起点逐级校验、逐级移交最后启动EL2的Hypervisor或EL1的富操作系统。这个过程不只是简单的bootloader它其实是一个“灵魂”级别的安全边界。当时我从ARMv7转过来看ATF时最大的感受是它不再是“一段把内核搬到内存里的代码”而是一整套运行时服务框架。你在别人代码里偶尔看到的SMCSecure Monitor Call、PSCI、OP-TEE、RMM这些词背后全都是ATF在支撑。用一句话概括ATF既是启动链路的可信执行者也是安全世界与普通世界之间的守门人。1.2 ATF在“通用系统”中的边界与职责ATF官方仓库全称是Trusted Firmware-A代码托管在GitHub上的ARM-software/arm-trusted-firmware。它并不负责引导内核也不负责装载文件系统它的边界是从SoC上电到EL2的Hypervisor跑起来之前的那一段“上帝时间”。具体到我理解和实操中的职责可以拆成这么几块建立EL3可信固件环境配置MMU、异常向量表、中断控制、栈空间为后续跳转提供干净的执行环境。加载并校验BL32安全世界的OP-TEE等、BL33U-Boot或UEFI镜像BL2阶段会做签名验证、镜像解密、内存布局规划。提供运行时服务通过SMC指令接收普通世界请求比如PSCI电源管理调用、或安全存储服务调用。维护安全世界与普通世界的上下文切换TrustZone世界中EL3和S-EL1的运行状态调度。许多人在网上搜ATF时会连带着搜“arm bl31 uboot”“arm交叉编译”“arm架构下的启动流程”这些词就是因为ATF和U-Boot的关系特别紧密。ATF的BL33一般加载的就是U-Boot或UEFI的固件之后U-Boot再去拉起内核。也就是说ATF虽然不直接引导内核但当它把链路打开后U-Boot才能安全地接管硬件。1.3 为什么源码评测和审计是有价值的源码评测这个词听起来挺“学术”实际上我每次拿到一个新平台或一块新板卡时第一件事就是读ATF源码里的平台初始化流程。你不可能一上来就凭空写一个platform_def.h至少得知道ARM官方平台是怎么设计的、SoC厂商的reference design里有哪些hook点。这种阅读本身就是一次安全工程审计的过程——你要判断厂商代码里有没有把不该暴露的端口映射到非安全世界、GICGeneric Interrupt Controller通用中断控制器的路由配置是否合理、有没有在SMC里留下不必要的处理函数。在这个维度上ATF源码是一个极好的学习材料。它的目录结构清晰bl1/、bl2/、bl31/、plat/、drivers/、lib/、services/每一个目录都负责一块独立能力。拿到一个新平台先看清plat/下对应目录的组织方式再配合docs/里的porting guide移植指南基本就能在几天内搭出可跑的最小系统。2. 五级启动链路深度拆解BL1、BL2、BL31、BL32、BL33各自干什么2.1 BL1与BL2的“两级可信根”设计思路我看很多人第一次读ATF源码时都被BL1和BL2的分工绕晕BL1也是BootROM之后执行的代码BL2也是两者的职责为什么不能合并这里的关键在于“信任最小化”和“物理存储约束”。BL1一般是固化在SoC内部ROM里的它体积极小只有几十KB几乎不依赖外部存储也难以被篡改。它的任务就两件把外部存储里的BL2镜像读出来做一次签名校验然后跳进去。BL2则运行在SRAM里它负责更复杂的事——从Flash、eMMC或FIPFirmware Image Package中解析出BL31、BL32、BL33等多个镜像逐个校验并规划它们的加载位置。在阅读代码时我会特别关注两个入口bl1_main()和bl2_main()。bl1_main()首先会调用bl1_early_platform_setup()完成最基本的UART和内存初始化然后就是BL1的ROM版本与RAM版本切换这个过程非常典型它体现了一种“先建立环境、再把自身镜像拷贝到更快/更大内存中继续执行”的固件设计思路。BL2的初始化里最关键的是bl2_plat_get_next_bl_params()这个函数是一个三级跳板用来收集所有待加载镜像的参数并处理加载地址。从安全工程审计角度BL1的信任根意义大于功能意义。因为BootROM阶段没有外部输入、没有交互接口攻击面小。如果厂商把太多的逻辑塞进BootROM反而会提高硬件缺陷修复的成本。ATF把BL1做得足够小、足够权威然后把弹性功能放在BL2上这个分层是一个经过实践检验的可靠设计。从源码阅读顺序上我建议先读docs/design/trusted-board-boot.rst了解TBBTrusted Board Boot的设计再去对照bl1/bl1_main.c和bl2/bl2_main.c。你会发现几乎每个函数背后都对应一个安全策略。2.2 BL31EL3 Runtime与SMC异常派发的核心枢纽BL31是整个ATF里最复杂也最精华的部分。它运行在EL3加载完毕后系统不会像BL1/BL2那样继续往下跳后释放自己而是常驻在内存中作为运行时固件持续提供服务。很多人在业务上遇到的“卡在启动”“SMC死循环”“OP-TEE无法初始化”问题都出在BL31的配置上。BL31里有三个我必须打重点标记的组件Runtime SVC框架这是BL31的“消息中枢”。普通世界EL1/EL2通过smc #0指令陷入EL3BL31根据SMC Function ID如0x84000000的PSCI标准调用路由到不同服务处理函数。PSCIPower State Coordination Interface电源状态协调接口它是一组标准接口定义了CPU hotplug、suspend/resume、system off/reset等操作。ATF实现了这些接口替Linux内核与底层硬件完成电源管理的最后一公里。Secure Monitor上下文管理BL31要保存和恢复普通世界的系统寄存器上下文当发生中断或异常需要切换到安全世界时它负责现场保护。我第一次把BL31跑起来时遇到了一个很经典的坑PSCI的CPU_ON调用总是返回“Invalid parameters”。排查后发现不是因为参数不对而是平台层的platform_setup()里没有正确配置Mailbox地址导致secondary CPU boot时无法读取到正确的入口地址。Mailbox机制简单来说就是一块约定的共享内存BL31启动secondary CPU之前把它的启动地址写到Mailbox里CPU在WFI唤醒后去读Mailbox再跳转。这个机制一旦没有落地PSCI的电话就打不通。2.3 BL32OP-TEE与BL33U-Boot/UEFI的安全协作BL32是可选的它不是所有平台都必须加载。但一旦系统要跑可信应用比如指纹、支付、DRM就需要加载一个安全世界OS——最常见的就是OP-TEE OS。ATF在BL2阶段会把BL32镜像放进安全内存区域然后BL31在启动时通过BL32_IMAGE_ID跳过去。BL32与BL31之间还有专门的SMC通道比如OP-TEE的std_svc服务、opteed服务这些都注册在BL31的runtime framework里。BL33就是我们最常见的U-Boot或UEFI。ATF启动U-Boot的常规路径是BL2加载BL31和BL33到内存中然后跳转BL31 → BL31完成必要初始化后跳转BL33。U-Boot启动后它就拥有EL2如果启用了Armv8虚拟化扩展就是EL2否则也可能是EL1此时ATF依然在EL3待命等待U-Boot或内核通过SMC发来的各种请求。有人会觉得“既然U-Boot也能做硬件初始化那ATF是不是多余”这个问题我在很多技术群里见过。答案是ATF不是多余而是安全边界上的必备承重墙。U-Boot这种非安全世界的bootloader不能被完全信任它有可能被篡改或被漏洞利用。ATF的存在就是在硬件层面约束U-Boot的权限边界同时为安全世界提供独立的运行环境。打个不精确的比方U-Boot是一个公司的前台负责接待引导ATF是办公楼里的保安部和监控室它不直接决定客人去哪但每一层楼的权限控制和异常处理都归它管。2.4 启动状态转移图让我用一个表格归纳很多人在读源码时会被各种宏和目录搞混我整理了一张自查用表格列出我在阅读和移植过程中最关注的几个关键点模块运行级别主要职责关键文件/目录常见坑点BL1EL3从BootROM接管加载BL2建立初始可信根bl1/,plat/plat/bl1_plat_setup.cBL1的RAM版本和ROM版本容易搞混BL2EL3解析FIP包校验并加载BL31/BL32/BL33bl2/,plat/plat/bl2_plat_setup.c镜像加载地址重叠导致校验失败BL31EL3运行时服务安全监控PSCIbl31/,services/,plat/plat/bl31_plat_setup.cMailbox和GIC配置缺失BL32S-EL1可信执行环境运行OP-TEEbl32/,services/spd/opteed/需要与BL31的SPD绑定BL33EL2/EL1U-Boot/UEFI引导内核plat/plat/bl33_*Entry Point必须与BL33编译地址一致这个表格基本就是我每次开始一个新平台移植时的检查清单。你会发现BL1和BL2虽然跑得快但它们的代码位置、数据位置有严格的SRAM大小限制。BL31则是真正长期站岗的“常驻模块”。BL32和BL33的加载顺序、加载地址直接决定了系统能否正常多级跳转。3. 安全固件工程审计一个可落地的ATF源码审查清单3.1 从TrustZone到SMC接口的攻击面审计很多团队做安全固件审计第一步是看芯片硬件手册里的TrustZone地址空间划分第二步就是对着ATF源码查“哪些外设被配置成了Secure哪些被配置成了Non-Secure”。如果某个DMA控制器、某个UART、甚至某个GPIO被错误映射到了Non-Secure攻击者就可能利用它绕过安全隔离。这个层面的代码主要散落在plat/plat/plat_setup.c和plat/plat/plat_security.c文件中它们会调用tzc400/tzc380等TrustZone Controller驱动完成地址区域的Secure/Non-Secure分配。对于SMC接口的审计我建议从services/目录入手。以PSCI为例它的入口是psci_smc_handler.c里面有若干个标准Function ID的case分支。如果一个平台在自定义实现里额外增加了非标准的SMC命令而对应的处理函数又没有做严格的调用者权限验证那这就是一个典型的高危风险点。审计时我会手动枚举代码里的所有SMC ID对照ARM FF-A或PSCI规范标记出规范之外的自定义命令再逐个分析其参数注入路径。3.2 安全启动链条与镜像校验机制审计TBBTrusted Board Boot机制是ATF安全能力的核心我在实际评估中会重点查三件事链上校验根是否存在ATF的信任根是平台在编译期注入的RSA公钥HashROTPK它被烧录在OTPOne-Time Programmable里。审计时要确认OTP中烧录的公钥是否与BL1中嵌入的版本一致签名校验算法是否至少是RSA2048或更好的ECC。每个镜像的签名算法和key slotFIP包中每个镜像都带有签名头ATF会调用auth_mod框架逐级校验。审计时要确认BL2只接受合法签名的BL31、BL32、BL33并且没有debug后门允许跳过校验。内存保护策略即便镜像签名是正确的如果加载到内存后被其他非安全外设可写整个安全边界就打折扣。所以要查ARM_MEM_PROT配置、DMA隔离配置。我参与过的两次审计项目中最常出现的问题是在FIP生成阶段用了“平台默认测试密钥”导致误以为已经启用了安全启动实际生产环境的密钥根本没烧进去。这种问题单纯靠读代码很难发现需要在板子上导出实际运行的ROTPK并进行比对。3.3 调试接口与日志泄漏风险ATF内部有很多调试性宏比如LOG_LEVEL、BL31_DEBUG这些宏在生产固件中应该关掉。我在审计中遇过某款设备的固件把LOG_LEVEL设为LOG_LEVEL_VERBOSE导致BL31启动时把DDR初始化时的物理内存布局、密钥地址等敏感信息通过UART打印出来。这类信息在逆向工程时极其有价值攻击者可以据此推断内存布局并构造攻击。此外JTAG/SWD调试口如果没在ATF阶段关闭也是一个高危面。ATF源码中有一些平台函数负责控制debug能力比如动态禁用调试接口审计时需确认这些能力是否被正确调用。只有在生产配置中禁用调试口ATF才真正具有隔离意义。3.4 固件更新机制与回滚保护在网关、汽车、物联网等场景里固件OTA更新是标配。ATF本身不直接实现OTA但它为可信更新提供了基础BL2负责校验新固件包BL31提供运行时安全存储服务。审计时要看平台的FIP更新逻辑是否与BL2的TBB校验闭环——也就是新固件必须带合法签名才能被BL2接受。更进一步回滚保护需要检查TRUSTED_BOOT_BOOT策略和ANTI_ROLLBACK计数器的实现防止攻击者把固件刷回含有旧漏洞的版本。有一次我在排查一块开发板的变砖问题时发现它用的更新脚本能把新FIP直接写入Flash却完全没有调用安全更新流程。这种“把ATF跑起来”和“把ATF安全用起来”之间的差距正是工程审计要解决的问题。4. 平台移植落地指南从zero start在一家新板卡上把ATF跑起来4.1 移植前的准备读懂Arm官方平台代码与构建系统如果你想把ATF移植到一块新的板卡上最笨也最稳妥的办法是找一块最接近的官方参考平台从它的代码改起。ATF官方仓库里典型的平台有qemu方便模拟、fvpFixed Virtual Platform用于无板调试、rpi3/rpi4树莓派学习友好、versal、juno以及各大芯片厂商放出来的对应实现。我个人的习惯是先用QEMU跑通一遍官方版再对照参考平台做新平台代码。QEMU的好处是开箱即跑能帮你把ATF与U-Boot、Image之间的启动交互搞清楚避免拿到真板后两眼一抹黑。从构建系统角度看ATF用Makefile组织编译核心变量包括PLAT平台名、DEBUG是否开启调试、LOG_LEVEL、BL33、BL32等。编译命令通常是make PLATqemu DEBUG1 BL33/path/to/u-boot.bin -j8如果要把OP-TEE加进来则需要提前编译出BL32镜像再通过BL32...传进去并在配置里打开SPDopteed。这里有个容易踩的坑如果不指定SPDBL31可能不会去加载BL32即使你给了BL32路径。因为BL31的启动流程里SPDSecure Payload Dispatcher就是它接触BL32的唯一中介没有SPD配置BL32只会作为镜像存在于FIP包中却不会被真正启动。4.2 平台抽象层你需要新建和修改哪些文件ATF的移植核心是plat/platform-name/目录下的一组文件。以我的经验最少必须准备的东西有这些platform_def.h定义平台相关的常量比如UART端口地址、DRAM基址、中断号、MAX_CPUS等。plat_setup.c实现plat_get_next_bl_params()、plat_setup()等用于系统初始化和跳转准备。plat_topology.c定义CPU拓扑结构实现plat_get_core_pos_by_mpidr()等函数。plat_psci.c或psci_helpers.S实现PSCI的CPU_ON、CPU_OFF等底层逻辑。plat_common.c提供platform_get_boot_source()等公共基础函数。plat_bl31_setup.c专门负责BL31阶段的内存、缓存和中断配置。写这些文件之前我会重点阅读docs/plat/porting-guide.rst官方移植指南和对应SoC的TRMTechnical Reference Manual。很多细节不要靠猜比如GIC的基址、Mailbox地址、SRAM大小、串口时钟等必须查硬件手册或参考厂商SDK确认。4.3 从一个完整的移植过程提炼出来的关键步骤下面我以一块使用Cortex-A53四核CPU、配有2GB DDR、UART0作为调试串口、GICv2作为中断控制器的简化板卡为例梳理一次真实移植过程的步骤。第一步生成最小平台目录复制plat/arm/board/qemu/目录重命名为你的目标平台名并保留但清空内部的子文件。这样能继承ARM通用宏和驱动框架你不用重写底层Timer、GIC驱动只需要替换平台地址定义。第二步修改platform_def.h重点检查以下宏#define PLAT_ARM_TRUSTED_SRAM_BASE 0x00000000 #define PLAT_ARM_TRUSTED_SRAM_SIZE 0x00040000 #define PLAT_ARM_DRAM_BASE 0x80000000 #define PLAT_ARM_DRAM_SIZE 0x80000000 #define PLAT_ARM_NS_IMAGE_OFFSET 0x82000000 #define PLAT_ARM_UART_BASE UART0_BASE #define PLAT_ARM_GIC_BASE GIC_BASE如果你的板卡上ATF运行的SRAM只有256KB0x40000但参考平台却定义成512KB超限后往往会导致启动异常、烧死、无法进BL31。这类问题只能通过抓BootROM打印和逻辑分析仪来排查成本极高。所以编译之前在文档里确认好SRAM大小是我吃过大亏后养成的习惯。第三步实现plat_setup.c把plat_arm_uart_init()中的UART地址改成目标平台值确认plat_get_next_bl_params()里BL33/BL32的加载地址与U-Boot中配置的链接地址一致。这个“地址一致性”问题几乎是我排查移植启动问题的第一大方向。第四步实现PSCI对于刚起步的板卡可以先直接使用plat/arm/common/里的标准实现再逐步改成自己的电源控制逻辑。对四核CPU来说CPU0由BL31启动时自然引导CPU1~CPU3的启动需要Mailbox我会在SRAM里保留一段固定的Mailbox地址并把它的物理地址写入platform_def.h。第五步编译验证命令参考make PLATmyboard DEBUG1 PLAT_UART_BASE0x10000000 BL33u-boot.bin -j8生成build/myboard/debug/bl31.bin等镜像后把bl31.bin替换到工作流中再用JTAG或串口看启停日志。如果是QEMU环境可以直接用qemu-system-aarch64 -machine virt -kernel bl1.bin一类的参数验证。4.4 移植完成后必须跑的安全与功能验证项移植跑通只是第一步我会建议至少做下面几项验证安全启动验证在BL2中启用TBB用平台密钥签名镜像验证所有镜像在没有合法密钥时无法启动。PSCI功能验证登录Linux后执行echo 0 /sys/devices/system/cpu/cpu1/online和echo 1 /sys/devices/system/cpu/cpu1/online确认CPU hotplug正常。同时测试systemctl suspend和reboot两个电源管理指令验证系统的suspend/resume路径。OP-TEE加载验证如果配置了OP-TEE启动日志中能看到BL32加载的条目并能运行optee_example里的hello_world。故障注入测试人为把BL33镜像改坏确认BL2校验能果断拦下来而不是继续往下走。这个测试成本很低但对安全审计来说极有价值。5. 我在移植和调试ATF过程中踩过的坑和总结的调试手段5.1 U-Boot链接地址与BL33加载地址不一致导致“假死”这是一个典型的“看起来像中断没配好、实际是地址错了”的问题。症状是BL31正常启动日志打到“Jumping to BL33”然后板子就黑屏、无输出按串口按键没反应。用JTAG读PC指针发现它跳到了一个未映射的内存区域或者跳进了一个看似有效但其实是数据的地方。查看U-Boot编译配置它的CONFIG_SYS_TEXT_BASE是0x81000000而ATF里BL33的entry point给的是0x82000000两者不匹配自然跑不起来。解决的办法就是统一入口地址并保持platform_def.h、U-Boot的链接脚本、以及FIP镜像生成时三者的与平台定义一致。这块没有捷径只能一遍遍对着内存map核对。5.2 GIC初始化顺序问题引发的Secondary CPU启动失败PSCI的CPU hotplug需要依赖GIC完成CPU中断路由和唤醒。如果一个平台在BL31阶段过早地操作GIC或者把GIC映射成了非安全地址就可能导致ARM Generic Timer无法触发SGISoftware Generated Interruptsecondary CPU一直在WFI里睡死。我调试期间发现plat_arm_gic_init()一定要放在pmu_setup()等依赖它的内容之前同时注意确认GIC的GICD_BASE和GICC_BASE是否正确。GICv2与GICv3的基址行为、寄存器和配置方式完全不同尽量用ATF自带的驱动不要手写。5.3 关闭编译优化或开低优化来查问题ATF本身在高优化等级下代码流程会被重排给调试增加障碍。我的习惯是先在DEBUG1 LOG_LEVEL50下编译必要时再配合-O0这样可以保留更多符号和变量信息也让反汇编里的函数边界更清晰。但要注意生产环境必须重新编译为高优化版本并调低日志等级。5.4 善用QEMU和OpenOCD做回归测试QEMU最大的价值不是跑完整个系统而是能快速验证ATF新代码的逻辑。我在修改平台文件后通常会先在QEMU上跑一轮启动、PSCI调用和系统重启确认没有引入低级错误然后再烧板。对调试来说OpenOCD JTAG是必备组合它能在BL31中下硬件断点读取EL3模式下各个系统寄存器的状态。有时候单靠打印日志根本看不出为什么SMC返回错误只有断点在smc_handler()里才能明确Function ID和调用来源。5.5 关于“复用官方示例代码”的边界我见过不少开发者为了让板子快速跑起来直接照抄SoC原厂或参考板的平台代码然后修修补补。这种方式在原型阶段无可厚非但一旦进入量产或安全要求较高的场景就容易埋雷。因为原厂代码里可能包含只在特定参考板上有意义的外设配置比如误导的TrustZone设置、错误的中断路由或者为了开发方便留下的“标准测试钥匙”。每次复用它之前都应该对着自己的原理图、硬件手册一条条核对。这个工作最初很烦但你越早做后面出安全问题的概率就越低。6. 一份可直接用于评估和移植的内部经验清单我在写这篇内容的过程中一直在回忆那些“如果当年有人直接告诉我我能少走很多弯路”的点现在集中列出来先搭在线环境再碰真板QEMU、FVP这类模拟器能帮你建立对启动链路的整体认知也能快速验证软件逻辑变更但它不能替代真板上的硬件依赖测试。两者是并行的而不是二选一。日志等级从高到低先全量打印了解正常流程再逐级关闭日志。不要把生产日志开成调试等级。所有地址必须三处一致ATF侧、U-Boot侧、FIP生成脚本侧的地址必须一致。不一致会导致非常诡异的启动问题并且排查起来极费时间。所有镜像都必须签名即使前期没有正式密钥也用测试密钥走一遍TBB的校验流程。不要在开发过程中把安全校验关掉否则后期打开时可能会漏掉一堆依赖“非安全启动”的隐患。安全审计不只是看代码还要看产线和交付流程ATF源码再干净如果产线烧录FIP的脚本没有禁止debug口或者密钥管理混乱最终产品依然不安全。如果说我在ATF的源码阅读和平台移植中收获最大的是什么那应该是ATF不是一个可以“跑起来就行”的普通软件它承载了整个系统的信任边界。你投入在安全配置和底层细节上的精力会在产品生命周期内以极高的倍率回报回来。上面所有内容都来自我看到、调试过、或亲身踩过的真实场景希望它们对正在啃ATF源码或准备移植平台的你有所启发。如果以后有时间我还可以把OP-TEE与ATF的协作启动流程单独写一篇那块又有很多值得聊的东西。