深入解析ARM Trusted Firmware:从BL31启动到安全固件移植实战 📅 发布时间:2026/9/6 10:56:58 👁 浏览次数: 1. 为什么几乎每一颗Arm SoC都绕不开ATF1.1 启动链路上的一次接力BL1、BL2、BL31各干什么ARMv8-A体系的启动是一个层层验证、逐步放权的流程。BootROM先加载BL1BL1是整个信任链的根体积很小主要做两件事初始化最基本的执行环境然后验证并加载BL2。BL2负责在DDR可用之后加载BL31EL3 Runtime、BL32Trusted OS和BL33U-Boot/UEFI并把它们连同证书一起打包成FIP镜像。BL31常驻在EL3承担后续所有的安全服务、PSCI电源管理和SMC分发。这里有个容易混淆的知识点很多工程师把ATF直接等同于BL31其实不准确。ATF仓库里完整的源码包含BL1、BL2、BL31三件套BL32是OP-TEE这种独立的可信OS项目。BL1、BL2的生命周期是阶段性的加载完下一级镜像后它们占用的内存就可以被覆盖BL31则是唯一需要长期驻留的安全固件。理解这个接力关系再去看ATF的代码组织思路会清晰很多。1.2 ATF源码目录结构与构建系统速览从git仓库clone下来顶层目录大体是这样的bl1/、bl2/、bl31/三段启动的源码实现plat/平台相关代码所有SoC厂商的适配都堆在这里drivers/启动期间用到的驱动主要是串口、GIC、定时器、TZASC等lib/核心库包括el3_runtime、psci、xlat_tables_v2、el3_common等services/各种SMC运行时服务比如标准服务、OP-TEE调度、SPM等tools/cert_create、fiptool等镜像生成和签名工具include/头文件平台宏定义、架构定义、服务描述符定义都在里面构建入口是顶层Makefile最小命令就是make PLAT平台名 DEBUG1。注意这里的PLAT不是arm64通用平台而是具体产品平台在plat/下对应的目录名。qemu、rk3399、zynqmp这些目录都是很好的移植参考。交叉编译这块多说一句ATF一般用aarch64-none-elf-或者aarch64-linux-gnu-工具链很多人在热搜里找“arm compiler 5.06”那个是Keil MDK对Cortex-M经典编译器的叫法和ARMv8-A上的ATF是两条完全不同的技术线。如果你在Cortex-A核上用Keil AC5编译那大概率是裸机或者RTOS场景与ATF的AArch64 GCC体系没有关系。拿到新板子之前先把工具链类型确认清楚能省掉一整天的时间。构建产物需要单独说明bl31.binEL3运行时固件bl2.binBL2镜像fip.bin把BL2、BL31、BL32、BL33以及安全证书打成一个FIP包如果开了LOAD_IMAGE_V2BL1会直接由BootROM加载BL2再去FIP里解析后面的镜像1.3 ATF在整个系统里到底管到哪一层可以用一个门卫的类比。BL1是保安室的钥匙只管开门并验证下一位的身份BL31是常驻监控室的值班员所有安全相关的来访都要经过它登记、分诊BL33才是前台接待负责用户看得见的操作系统启动。把权限分清楚之后ATF的代码也就好读了EL3的代码全部属于特权级别最高的一层操作系统内核在EL1Hypervisor在EL2它们想请求安全服务只能通过SMC指令陷到EL3再由BL31分发处理。这也就是为什么ATF的代码量不大但是几乎每个Arm服务器、手机SoC、嵌入式平台都离不开它。2. 深度源码评测BL31就是那颗心脏2.1 SMC调度机制来自非安全世界的那一声敲门ARMv8-A的EL1/EL2代码执行smc指令会触发同步异常陷入EL3。BL31的异常向量表在bl31/aarch64/runtime_exceptions.S里拿到smc_fid之后的第一步不是直接处理而是查一张“运行时服务描述符表”。ATF定义了一套运行时服务描述符存放在rt_svc_descs段。每个描述符包含服务的起始ID、结束ID、初始化函数、运行时处理函数和调用的异常级别。源码里通过DECLARE_RT_SVC宏把结构体放到指定段BL31启动时用runtime_svc_init统一注册。SMC调度时根据服务ID去查这张表命中后就调用对应的svc函数指针。这个机制很像操作系统里的系统调用表Linux有sys_call_tableUEFI有runtime service table本质都是同一套查表思路。服务ID的布局可以参考SMCCC规范。bit[31]表示这是32位还是64位调用bit[30]区分Fast call和Yielding callbit[29:24]表示调用的归属方比如标准安全服务、可信OS、SoC厂商自定义服务等。真正处理请求之前ATF还会做一系列边界检查包括传入的参数个数、调用方异常级别是否匹配、服务是否允许该来源访问。安全审计时优先关注的就是这些检查有没有被绕过。2.2 PSCI你点的每个电源按钮都经过这里PSCI可能是普通工程师接触最多的部分。U-Boot里执行psci_cpu_on的时候最终会通过SMC把请求转给BL31。BL31的lib/psci目录下实现了完整的PSCI状态机从cpu_on、cpu_off、cpu_suspend到system_reset、system_off都有对应入口。收到请求后PSCI框架会调用平台层提供的psci_plat_pm_ops函数指针比如上电某个CPU核时会调用pwr_domain_on回调由平台代码操作对应的Power Controller寄存器。这也引出了热搜词“arm bl31 uboot”背后的话题。U-Boot为了兼容旧固件很多平台会在U-Boot里内置一个PSCI模拟层。启动时U-Boot会检测固件是否支持PSCI如果支持就通过SMC调用BL31不支持就退回在U-Boot内部模拟CPU电源管理。所以在做平台移植时一定要确认BL31确实实现了PSCI并且把DTB里psci节点的版本号报高一点否则Linux内核可能根本不走PSCI电源路径而是依赖U-Boot的老方案。2.3 上下文管理安全世界和普通世界的切换由谁保护BL31不仅要处理服务请求还要保证从EL3返回EL1/EL2时通用寄存器、系统寄存器、内存状态都完全正确。context_mgmt库维护了cpu_context结构体里面保存通用寄存器、SPSR、ELR_EL3、TLS寄存器等内容。每次从Normal World进入Secure World再返回时BL31都要完整保存和恢复现场。这套上下文切换在带有OP-TEE的平台上尤其明显。Normal World请求安全服务后BL31保存Normal侧上下文切换到Secure World上下文把控制权交给BL32OP-TEE完成工作后触发SMC回到BL31BL31再把Normal侧上下文恢复。如果这套切换逻辑出了bug表现出来就是偶尔的系统崩溃、寄存器值被篡改、或者安全世界内存被异常写入。排查这类问题时建议直接打开EL3的crash日志BL31在异常时会打印PC、LR和关键寄存器顺着这些信息基本能定位到是上下文保存还是MMU映射的问题。3. 安全固件工程审计怎么审从信任根到边界检查3.1 把信任链画清楚审计就有抓手审计ATF固件第一件事不是急着找漏洞而是把信任链画清楚。从BootROM到BL1BL1验证BL2的证书BL2验证BL31、BL32、BL33最终镜像里的每一段都有对应的签名或哈希。开启TRUSTED_BOARD_BOOT之后tools/cert_create会生成X.509证书链fiptool把这些证书和镜像打包成FIP文件。审计时重点看这么几个位置根公钥存在哪里通常是烧进eFuse或者编译进BL1。证书验证分支是不是完整有没有哪里可以通过修改FIP的元数据跳过校验镜像解析时对长度、偏移的边界检查是否严格TSB安全固件审计里常见的一个问题是“解析太宽容”比如用memcpy时长度来自镜像头却没验证长度是否越界攻击者完全可以构造一个畸形FIP把数据写到预期之外的内存。结合“固件安全”这个热搜词多说一句固件安全的本质是信任边界管理。ATF的最小可信计算基是BL1加上BL31的一部分运行时代码再加上证书验证逻辑。TCB越小可被攻击的面越小。BL1和BL2在完成使命之后主动放弃资源、可以被覆写就是这种设计思路的直接体现。所以审计时要特别注意BL31启动后BL2用过的栈、页表和缓冲区有没有被及时清零或者隔离否则可能留下残留数据泄露。3.2 内存隔离TZASC和MMU配置是安全的地基TrustZone技术的核心目标之一是不让Normal World通过CPU或者DMA随意访问安全内存区域。ATF通过驱动层操作TZASC控制器把物理地址空间划分成安全区和非安全区非安全侧的访问会被硬件直接拒绝。在代码审计中需要确认以下对应关系安全世界MMU映射的物理地址段是否不大于TZASC划分的安全段Normal World是否有路径通过DMA发起对安全内存的写操作xlat_tables_v2里每个region的权限属性是否正确有没有把设备寄存器映射成可写的内存给攻击者提供了改配置的入口ATF的MMU初始化用的是lib/xlat_tables_v2库。它把地址翻译和内存属性配置封装成了mmap_add_region、mmap_add这类接口使用起来并不复杂但配置不当就会出大问题。常见错误包括把设备寄存器映射成Normal memory导致DMA一致性问题或者把安全内存的映射权限设置成了可读写但没加安全属性标志。3.3 一份可以落地的ATF安全审计清单结合我实际做过的固件安全评估整理一份简化版的检查清单所有SMC handler是否校验调用来源。EL1来的调用、EL2来的调用、Secure侧来的调用分别要什么权限ATF有没有严格区分有没有整数溢出、数组越界、未初始化变量这类问题在plat目录下尤其常见FIP解析代码对镜像长度、偏移的边界检查是否完整运行时服务描述符表是否允许重复注册服务ID冲突时会不会产生非预期行为发布版本里是否残留调试后门、内存dump接口、未禁用的测试SMC服务中断处理路径上GIC配置有没有把非安全中断误路由到Secure侧BL31上下文切换时有没有遗漏保存某个系统寄存器工具层面我常用Ghidra或者IDA对bl31.bin做静态逆向同时用qemu把固件跑起来做动态调试通过串口日志观察SMC调用的入参和返回。很多ATF的审计结论不是靠自动化工具扫出来的而是靠把一个SMC服务从入口到返回的数据流完整跟一遍才发现的。尤其在厂商自定义的SIP服务里往往藏着最多的审查盲区。4. 平台移植落地把ATF接到新SoC上的最小改动集4.1 最小平台目录先让BL31说出第一句话新平台移植的第一步是建目录plat/vendor/soc/然后实现三个核心部分platform_def.h各种宏定义包括DDR地址、UART基址、中断号、堆栈大小等平台初始化函数bl31_early_platform_setup、bl31_plat_arch_setup、bl31_platform_setup三个函数按顺序被调用串口驱动适配让printf有输出这是Bringup阶段最重要的反馈手段BL31的入口是bl31_entrypoint它经过el3_entrypoint_common的初始化之后会依次调用上面三个平台函数。这三个函数很好记早期平台设置负责拿到平台参数并准备好串口架构设置负责配置MMU并映射内存平台设置负责初始化外设、电源管理、GIC等。编译时打开DEBUG1把LOG_LEVEL调到50以上就能在启动后看到BL31的banner和每步初始化日志。如果你用的是某款基于Cortex-A72搭配GICv3的SoC可以先复制plat/qemu的目录结构改。qemu平台代码短小但五脏俱全从平台宏定义到PSCI回调、GIC配置、串口输出都有是我见过最适合作为移植起点的参考实现。4.2 内存映射、栈与堆地址配错一切白搭在bl31_plat_arch_setup里用xlat_tables_v2的接口把设备内存和DDR空间映射好。这里最常见的两个错误是把安全DDR映射成了Device memory导致性能骤降以及把设备寄存器映射成了Normal memory导致register write被缓存或者顺序错乱。经验值如下MMIO寄存器用MT_DEVICE属性不用开cache普通DDR用MT_MEMORY属性按需加MT_SECURE或MT_NS标识共享内存区域用MT_MEMORY | MT_NS确保Normal World可访问但同时要在TZASC里划成NS区间栈和堆的大小也要提前算好。BL31的栈在platform_def.h里通过PLATFORM_STACK_SIZE定义堆则是用来给xlat tables分配页表空间的。页表大小取决于映射区域数量如果映射很多段建议直接把堆开大一点避免运行时动态分配失败。4.3 GIC中断路由安全中断怎么到EL3ATF的GIC驱动在drivers/arm/gic/下支持GICv2和GICv3。移植时先确认硬件版本然后实现对应的平台配置函数。GICv3的配置和GICv2差异很大特别是在中断分组和唤醒逻辑上不建议直接把v2的代码往v3上套。BL31启动后会初始化GIC把Group 0中断路由到EL3把Group 1中断留给Normal World。做中断验证时可以注册一个安全SGI的handler然后手动触发SGI看能否进入EL3的中断处理流程。这一步通过后再联调U-Boot和Linux的中断路径会更从容。如果在GIC初始化时遇到“CPU接口没使能”的问题通常不是中断号配错而是GICD_CTLR和GICC_CTLR的设置顺序不对。4.4 BL32与BL33的衔接把U-Boot请进门拿到BL33的entry point之后BL31在完成所有运行时服务初始化后通过el3_exit跳转到Normal World的BL33。U-Boot启动后Linux内核里的PSCI操作又会通过SMC回到BL31完成CPU热插拔、深度睡眠、系统重启等电源动作。整条链路中牵涉到DTB里的psci节点必须配置method为smcversion至少0x00010000。移植时最常忘的就是没给DTB加psci节点Linux会认为平台不支持PSCI直接退化到无电源管理模式。很多SoC还带系统控制处理器SCPBL31与SCP通过共享内存或者mailbox通信把CPU簇的电源控制交给SCP去执行。此时BL31的平台pm_ops回调里需要实现SCMI协议交互。这块往往是整个移植过程中最耗时的部分建议先把无SCP的最小场景跑通确认BL31能启动、能进U-Boot、能启动Linux再逐步把电源管理交给SCP。5. 移植调试中的真实踩坑串口哑巴、MMU失效、热插拔崩溃5.1 串口没有输出的完整排查链路BL31移植后最常见的问题就是“完全没有串口日志”。网上很多人的第一反应是UART驱动没配对但实际排查链路往往要长得多确认BootROM确实把BL31加载到了目标地址。可以在BL31入口的第一条指令处加GPIO翻转用示波器看电平变化确认到达BL31时UART时钟是否使能。很多SoC的调试串口默认没有使能时钟BootROM打印完就关了确认pinmux配置正确。同样的UART外设可能有多组引脚复用默认GPIO状态不一致确认到达BL31时UART控制器有没有被BootROM意外关闭确认波特率是否匹配。BL31的PL011或者其他UART驱动对分频配置很敏感排查这类问题时日志不是唯一手段。GPIO翻转、JTAG调试器、逻辑分析仪抓UART波形都能用。最忌一上来就怀疑某个环节然后盲目改代码而是把整个链路按阶段划分找到第一个失效点。5.2 开启MMU之后系统直接跑飞这是ATF移植里最经典的坑。症状通常是BL31启动日志打印到某一行然后系统死掉或者跳到莫名其妙的地方。这时不要怀疑是随机性崩溃先从这几个方向查。第一BSS段是否被正确清零。链接脚本里的零初始化区如果没清静态变量初始值就是垃圾值GIC配置、电源状态机全都会错。第二开启cache之后MMU映射的内存属性类型不对。寄存器被映射成Normal memory后写入可能被缓存导致外设永远停留在旧状态。第三链接脚本里加载地址和执行地址是否区分清楚。BL31从DDR加载后如果LMA和VMA不一致代码跳转会落到空位置。推荐一个调试技巧先只开MMU和I-Cache不使能D-Cache跑通了再逐步开。这样可以缩小问题范围避免cache和MMU的问题混在一起干扰判断。5.3 CPU热插拔执行一次后系统挂死这个坑在实测中最折磨人。第一次调用cpu_off正常第二次cpu_on之后系统就挂死。问题往往不在PSCI主逻辑而在平台层的pwr_domain_off回调。常见原因有CPU送出WFI之前GIC里该核的PPI中断没有正确屏蔽关闭CPU电源时没有先把该核的GIC Redistributor状态保存或清理重新上电后CPU的入口地址没有设置导致warm reset后跳到错误位置排查时在psci_cpu_off路径上加状态机打印把每个power domain状态转换过程拉出来看。同时确认平台上电CPU时执行的是cold boot流程还是warm boot流程warm boot会直接跳到BL31的warm_boot_entrypoint走的是另一套初始化逻辑。还有一个容易忽略的点PSCI的CPU_ON参数里entry point地址必须是物理地址而且对于AArch64来说应该是一个带高地址位的64位地址。很多早期平台用32位地址传参第一次能过第二次就会因为地址截断而挂死。最后再分享一点实际体会ATF这个项目和U-Boot很像官方文档永远停留在架构层面真正的细节全部埋在代码里。我刚上手做平台移植的时候对BL31里各种SMC服务调度也是一头雾水后来就是靠把每个SMC调用的入参、返回值、错误分支全部用日志打出来才一点点把坑填平。建议新人先把qemu平台上跑通BL1、BL31、U-Boot、Linux的完整启动链再上真板子。如果手里已经有现成平台直接对着plat/qemu或者plat/rpi3的代码来移植比从零手写快得多。遇到问题的时候不要急着怀疑ATF内核代码先把平台层三个setUp函数的日志打开一半的问题当场就能定位。另外交叉编译工具链版本也会影响ATF构建。GCC 10以上的版本对AArch64的默认行为有调整如果遇到莫名其妙的编译错误先检查工具链版本和ATF版本是否匹配。这个经验我反复提到因为几乎每个新项目都会有人在这上面浪费半天时间。ATF的世界比U-Boot更安静但一旦进入状态你会觉得这个仓库的设计确实精巧值得花时间读透。