ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ARM可信固件ATF源码深度解析与平台移植实战指南

ARM可信固件ATF源码深度解析与平台移植实战指南 如果你拿到一块新的ARM开发板上电后串口只输出几行日志就卡死先别急着怀疑内核和u-boot——大概率问题出在比它们更早的EL3固件层也就是 Arm Trusted Firmware-AATFArm官方仓库里叫TF-A。我最近在一块基于Cortex-A72的板卡上做平台移植前后和ATF源码死磕了小半个月把BL1到BL31的启动链路、PSCI实现、内存映射以及平台移植要动的文件逐一翻了个遍。这篇文章把这次“深度源码评测”的完整过程写出来先梳理架构全景再按工程审计的思路过关键模块最后给出一套可以直接照着做的平台移植落地流程。无论你是做板级BringUp、搞安全固件评估还是第一次想在QEMU上把ATF跑起来这篇都能当一份绕开弯路的地图用。1. 启动链路上的“接力赛”ATF的架构全景和它到底管了什么1.1 一次上电后的四棒接力BL1、BL2、BL31、BL33ATF启动时做的事情本质是一场按信任级别排好的接力赛。芯片上电后最先执行的是固化在SoC内部ROM里的BL1它负责最基本的硬件初始化比如设置异常向量表、初始化一点SRAM、把下一棒BL2从存储介质里搬到安全SRAM里执行。BL2负责更完整的平台初始化并负责加载BL31EL3运行时固件、可选的BL32TEE比如OP-TEE以及BL33通常是U-Boot或UEFI。这四级的命名很容易让新手头晕我一般这样记BL1是“最小信任根”BL2是“加载器”BL31是“常驻安全监视器”BL33是“非安全世界的下一个跳板”。很多做Linux应用层开发的人会问为什么不能直接让引导代码跳到U-Boot因为从安全角度讲如果主系统镜像被篡改攻击者可能直接拿到所有特权而从工程角度讲CPU的时钟、DDR控制器、GIC这些资源需要分级初始化BL1和BL2只跑在SRAM里DDR还没有初始化这本身就是一种安全边界。EL3是ARMv8架构里权限最高的异常级别运行在EL3的BL31一旦常驻就会成为一个横跨安全世界和非安全世界的“看门人”后续OS发出的PSCI电源管理请求、安全监控调用都会先落到EL3手里。1.2 FIP镜像和描述符表ATF如何“点菜”加载下一棒ATF并不像很多人想象的那样把BL31、BL33等镜像一股脑塞进固定地址就能跑。它会用FIPFirmware Image Package把多个镜像打包到一个文件里相当于把一桌子菜都放进一个保温箱。平台在启动时通过fip工具和平台代码里定义的“镜像描述符表”来确定该加载哪些镜像、分别加载到什么地址。这里的核心数据结构是image_info_t和bl_params_t。BL2在运行时会把平台描述好的参数链表逐个解析对每个镜像做认证开启了Trusted Board Boot时再搬运到目标内存。我做移植时最容易被坑的点是镜像加载地址和运行时地址是两个概念BL2从存储里把BL31读到一个临时加载地址可能还要做一次重定位最后才跳到BL31的entry point。如果平台里这两个地址配错能看到的现象就是“BL2打印显示BL31加载成功但跳转后串口再也没输出”。1.3 异常级别与安全世界的边界EL3为什么是地基ARMv8-A定义了四个异常级别EL0用户态、EL1OS内核、EL2虚拟化、EL3安全监视器。普通Linux最多用到EL2而ATF的BL31常驻在EL3。这里的关键认知是EL3不是“EL2上面多一层”那么简单它是Secure World和Normal World之间唯一的切换点。一个安全世界的OP-TEE和一个非安全世界的Linux它们之间的通信、上下文保存恢复、中断路由都绕不开BL31的监控模式。为什么安全固件工程审计一定要重点看EL3因为一旦EL3代码被攻破所有世界隔离都失去意义。ATF自身也采用了很多缓解手段执行权限页表隔离、只读数据段保护、设备内存严格按Device-nGnRnE属性映射等。在源码层面看它的bl31_main()启动流程会先建立完整的MMU映射再把自身数据段设置为只读最后才注册并分发runtime服务。很多初学平台移植的人图省事把EL3空间的MMU映射写得异常宽松这种“能跑就行”的做法在安全评估里是要被打回重做的。2. 源码级工程审计从BL31执行流到PSCI与MMU的底层逻辑2.1 bl31_main一个常驻服务的“调度中心”找到bl31/bl31_main.c里的bl31_main()函数你就能看到ATF运行时固件的骨架。它做的事情可以概括为先调用bl31_early_platform_setup2()做最基础的串口、内存、中断控制器初始化再通过bl31_plat_arch_setup()建立EL3的MMU映射随后注册runtime services最后通过bl31_prepare_next_image_entry()准备跳转到BL33。runtime services是BL31最有意思的机制。每一个服务比如PSCI、标准服务、SDEI、SPMD都通过DECLARE_RT_SVC宏注册到rt_svc_descs数组里。当非安全世界执行smc #0指令时CPU会陷入EL3BL31的synchronous_exception入口会解析SMC function ID根据里面的服务IDOEN字段找到对应的服务描述符再调用它的smc_handler。这套机制很像操作系统的系统调用表但更精简。我审计时往往会先数一遍rt_svc_descs数组里到底注册了哪些服务因为没必要的服务等于多余的攻击面。2.2 PSCI不是Linux的专利EL3手里的CPU“生死簿”Power State Coordination InterfacePSCI也许是BL31承载的最重要功能。CPU的开关、挂起、系统重启和关机全是ACPI或DTB里向BL31发PSCI调用完成的。为什么这些权力必须交给EL3因为电源状态切换涉及上下文保存、中断路由、缓存维护等操作需要在安全态执行而且多个核心的启动顺序必须由一个可信实体统一调度。ATF的PSCI实现在lib/psci/psci_main.c里入口是psci_smc_handler()。我第一次做多核启动调试时就是被psci_cpu_on()卡了很久。现象是Linux启动时只有主核在跑psci_cpu_on调用后从核始终没进入内核。后来查下来原因是BL31初始化GIC时没有正确配置从核的中断亲和性加上平台代码没有实现plat_secondary_cold_boot_setup()从核的入口地址没有设置成功。这类问题在源码审计里要格外关注CPU hotplug、suspend/resume不是“板子能起Linux”阶段就能验证的跑到压力测试或热迁移时才容易暴露。2.3 页表与MMUxlat_tables_v2如何保证EL3内存隔离ATF的MMU代码是lib/xlat_tables_v2/这套代码同样被BL31和BL2使用。它提供了一套比Linux内核简洁得多的页表构造API核心思路是先用mmap_add_region()收集内存映射区域再调用init_xlat_tables()生成页表。对于EL3固件来说内存映射必须遵循最小权限原则代码段可读可执行、数据段可读可写但不可执行、设备区域使用设备内存属性而不是普通内存属性。平台移植里最常见的配置项是PLAT_PHY_ADDR_SPACE_SIZE和PLAT_VIRT_ADDR_SPACE_SIZE它们决定页表映射的地址范围大小影响一级页表需要的SRAM空间。很多板子SRAM紧张如果这两个值配得过大链接时就会出现页表存放区溢出配得过小访问超出范围的地址又会触发Translation Fault。这块没有捷径可走只能对照板子的实际DDR和MMIO地址范围一个个算清楚。2.4 安全启动与TBBR信任链的密码学实现ATF的安全启动叫做Trusted Board BootTBBR它是一条典型的密码学信任链。BL1被固化在ROM里内置平台根公钥BL2镜像由平台私钥签名BL1在加载BL2时验证签名和证书链BL2再用同样方式验证BL31、BL32和BL33的证书与镜像。这套逻辑分布在drivers/auth/目录下整个流程依赖证书解析X.509、哈希校验、RSA验证等模块。实际做平台产品时TBBR往往是最后才打开的开关因为它要求先规划好密钥体系和OTP熔丝。但源码审计时一定要看它因为即使目标板子不开TBBR代码里认证过程的实现质量也能反映整个固件的安全水准。我检查过一些第三方BSP有的直接把认证函数做成“永远返回成功”这种土办法在开发阶段省事在量产设备上等于给攻击者开了一扇大门。如果做安全评估这是绝对的红线问题。3. 平台移植前的功课目录结构、平台定义文件和最少改动原则3.1 先看官方平台怎么写FVP和QEMU是免费老师ATF源码仓库里platform目录按厂商组织plat/arm/board/fvp是Arm官方固定虚拟平台的完整实现plat/qemu是QEMU虚拟机的轻量实现。我强烈建议第一次做平台移植的人先从QEMU平台入手把整个编译、烧录、观察串口输出的循环跑通再动自己的板子。QEMU平台代码短小精悍不需要处理真实DDR初始化、真实GIC布线这些复杂问题能让你把主要精力放在理解ATF启动流程上。官方平台目录通常包含这几个文件platform_def.h所有地址和配置宏、plat_common.c通用平台函数、plat_bl31.c或bl31_plat_setup.cBL31相关的钩子、plat_topology.c核心拓扑描述、以及GIC、串口、定时器等外设驱动封装。移植的第一步不是开始写代码而是把官方平台里与SoC强相关的部分识别出来画一张“哪些能用、哪些必须改”的清单。3.2 平台移植必须实现的钩子函数ATF定义了一组平台操作接口分散在include/plat/common/platform.h里平台代码必须实现。我挑了最关键的几个整理成一张速查表接口出现阶段职责bl31_early_platform_setup2()BL31入口初始化串口、内存、GIC基础bl31_plat_arch_setup()BL31启动中建立EL3 MMU映射plat_get_next_bl_params()BL2阶段填写下一棒镜像的描述符表plat_get_bl_image_load_info()BL2阶段提供FIP里镜像的加载布局plat_get_my_entrypoint()CPU启动返回CPU入口地址用于从核启动plat_setup_board()BL31启动中板级外设初始化我通常建议第一步只实现一个“最小集合”能初始化串口、能设置内存映射、能加载BL33。等你看到BL31日志里出现Booting BL33再慢慢补齐GIC、PSCI、休眠唤醒这些功能。一上来就想把官方平台的所有钩子填满很容易被编译错误淹没。3.3 platform_def.h里的硬数据地址、容量、外设地址平台移植中80%的启动失败都源于platform_def.h里某个地址配错。这里有几组数据要特别小心。第一组是BL镜像本身的地址BL31_BASE、BL31_LIMIT、BL2_BASE、BL2_LIMIT它们必须落在平台可用的SRAM或DRAM范围内且有足够容量不能和别的东西重叠。第二组是内存映射范围PLAT_PHY_ADDR_SPACE_SIZE、PLAT_VIRT_ADDR_SPACE_SIZE决定MMU能覆盖的物理和虚拟地址范围。第三组是外设地址串口UART基地址、GIC Distributor/Redistributor基地址、定时器基地址。这些地址必须对照芯片手册或设备树里的reg属性去核对不能直接抄其他板子。我记得有一次把GIC基地址的某个低bit拼错整个系统在启动后第一次打开中断时就触发异常而且异常发生在EL3串口打印几乎抓不到有效信息。后来是靠QEMU模拟器逐条对比寄存器的读写结果才定位到问题。所以这里想强调一点宁可多花半小时对着芯片手册核对platform_def.h也不要拿到板子之后靠猜来调试。4. 移植落地实操从最小平台到跑通BL31加BL334.1 先用QEMU把“最小可运行平台”跑通真实板卡移植之前我建议你先在QEMU上把ATF的全套启动链跑熟。QEMU的virt机器支持qemu-system-aarch64直接加载ATF生成的bl1.bin和fip.bin编译指令并不复杂make PLATqemu DEBUG1 CROSS_COMPILEaarch64-linux-gnu- BL33/path/to/u-boot.bin编译完成后在build/qemu/debug/下会生成bl1.bin和fip.bin。启动命令可以简化成qemu-system-aarch64 -machine virt -cpu cortex-a57 -m 1G \ -bios build/qemu/debug/bl1.bin \ -display none -serial stdio第一次看到QEMU串口里依次出现BL1、BL2、BL31的日志时整个启动链的基本印象就建立了。之后在QEMU上改platform_def.h、加调试打印、尝试破坏某个地址来观察崩溃行为成本都极低。等你在QEMU上把ATF跑得滚瓜烂熟再面对真实板卡时至少能排除掉一半“其实和硬件无关”的软件配置问题。4.2 创建自己的平台目录从复制开始真实平台移植第一件事是创建plat/vendor/board/目录然后把QEMU或某个结构相近的官方平台代码复制过来改。我不建议从零手写因为ATF对链接脚本、编译flags、内存布局有很多隐含约定从零写的成本非常高。复制之后先改platform_def.h里所有和内存、串口、GIC相关的宏再改plat_common.c里初始化外设的部分最后处理多核相关的plat_setup_topology()和plat_get_my_entrypoint()。编译命令变成了make PLATmyboard DEBUG1 CROSS_COMPILEaarch64-linux-gnu- \ BL33/path/to/bl33.bin如果平台使用较新的ATF版本还可能遇到FEATURES相关的配置比如是否支持RMERealm Management Extension、是否支持SPMD等。这些功能默认是关闭的如果不需要不建议在第一次移植时打开先让基础启动链跑通再逐步加功能。4.3 打包FIP并把BL33接进来ATF运行时固件不直接跳去执行Linux内核而是跳到一个BL33——大多数场景下是U-Boot或UEFI。BL33的入口地址必须和平台代码里的PRELOADED_BL33_BASE保持一致如果BL33是U-Boot它通常会把自己链接到一个固定地址比如QEMU上常见的0x60000000或者真实板卡的0x80000000。BL2加载BL33时只是把FIP里的BL33镜像解包拷贝到指定地址然后BL31在runtime阶段跳过去。打包FIP的命令需要列出所有镜像tools/fiptool/fiptool create \ --tb-fwbuild/myboard/debug/bl2.bin \ --soc-fwbuild/myboard/debug/bl31.bin \ --nt-fw/path/to/u-boot.bin \ fip.bin之后把bl1.bin和fip.bin按平台要求写入Flash或SD卡。很多板子把BL1写在BootROM之外的一片SPI NOR里地址和大小由厂商SDK决定。这里最常见的错误是Flash布局和ATF编译时默认的FIP_BASE不匹配导致BL2找不到FIP。记得对比芯片手册里的Flash Memory Map和platform_def.h里的定义。4.4 与主流ARM生态的集成方式在实际产品里ATF很少单独亮相。以国内常见的ARM服务器SoC为例用户拿到手的往往是UEFI固件而ATF已经作为UEFI启动链的一部分被编译进去或者由板级BSP在UEFI之前先执行。像飞腾、鲲鹏这类平台基于ATF的启动固件通常由芯片厂商提供二进制普通OS开发者几乎感知不到ATF的存在但一旦做板卡定制或者需要修改电源管理策略就又得把ATF源码捡起来重新编译。如果你接触的是华为、麒麟这类软硬件生态也会发现它们常常会基于上游ATF版本做定制修改PSCI策略、加自研安全服务等。这时源码评测的意义就体现出来了不要轻信“ATF是上游原版”这种说法用git log看补丁、用反汇编确认实现的PSCI call才能真正摸清楚一套固件的底细。5. 调试手段与经典故障没有硬件调试器时怎么办5.1 串口打印往哪放日志比你想的更讲究ATF的log系统可以在构建时通过LOG_LEVEL调整从LOG_LEVEL_NONE到LOG_LEVEL_VERBOSE共五档。开发阶段我习惯开LOG_LEVEL_VERBOSE但量产固件务必关掉或者降到LOG_LEVEL_INFO每一条多余的日志都是信息泄露面。串口初始化的时机也很有讲究BL1阶段只能使用BL1自己的console等跳到BL2、BL31时各阶段要分别调用自己的console驱动。如果你发现BL2日志正常但BL31完全没有输出先检查是不是BL31阶段的console基地址被MMU映射成了错误外设或者GIC、时钟配置导致串口时钟频率不一致、波特率漂移。另外很多SoC的UART在EL3视角下的地址和Normal World视角下的地址不一样比如有些芯片的UART在非安全地址有一个别名在安全地址也有一个别名。如果BL31使了安全别名而BL33用的是非安全别名两边打印格式会不一致。这种情况不是bug但会让你误以为“UART没驱动起来”。5.2 用QEMU/FVP做无板调试没有一个通用JTAG调试器并不可怕ATF配套的FVPFixed Virtual Platform和开源QEMU都能模拟出足够真实的运行环境。在QEMU上除了能看到串口日志还能用-d in_asm -D debug.log把每次执行的汇编指令记录下来。当BL31跳转后完全没日志时我可以直接看最后几条指令是什么判断是跑飞了、进入了异常还是卡在某个循环里。FVP则更贴近真实硬件行为支持GIC、中断、多核启动的精细模拟缺点是二进制比较大、配置稍复杂。我的习惯是能上QEMU的先上QEMUQEMU复现不了的问题再开FVP。两者配合基本能覆盖启动流程、PSCI、MMU映射、中断路由这几类核心问题的排查。5.3 高频踩坑地址映射、GIC配置、BL33入口整理一下我这次移植和平时帮人看问题最常遇到的三个坑。第一个坑是MMU映射的权限和内存类型错误。最典型的表现是BL31在访问某个外设寄存器时触发Synchronous Abort或者在跳转BL33时出现Instruction Abort。排查方法是把mmap_add_region()的每一条映射都对照外设手册检查确保设备区用MT_DEVICE、普通内存区用MT_MEMORY可执行区域才给MT_EXECUTE。第二个坑是GIC配置不正确。BL31要初始化GIC才能把中断路由到正确世界、正确核心。很多板子在U-Boot阶段才会初始化GIC但如果ATF的PSCI要支持从核启动BL31阶段就必须把GIC配好。否则从核一启动就卡在中断等待上。调试这一类问题我建议在bl31_plat_arch_setup()之后马上打印GICD的PIDR寄存器、GICR的WAKE寄存器确认GIC地址正确且已上电。第三个坑是BL33入口地址和模式不匹配。BL33可能是32位代码在某些兼容平台上也可能是64位U-Boot/UEFIATF跳转时会把CPU状态设置在对方期望的模式下。如果BL33入口地址写错或者SCR_EL3.RW位设置不对Linux/U-Boot根本不会执行。入口地址尽量从BL33的链接脚本或U-Boot的CONFIG_SYS_TEXT_BASE里去确认不要去设备树里猜。6. 从安全固件视角做审计攻击面、信任根与加固检查6.1 为什么说ATF是一个“影子操作系统”BL31在EL3常驻运行它在系统运行时几乎不被普通程序感知但所有SMC请求、快速中断FIQ都可能在某个瞬间切换进EL3。从这个角度说ATF就是一个“影子操作系统”它有中断入口、有服务注册表、有自己的一整套内存管理逻辑只是它不面向用户也不向开发者暴露API。安全固件工程审计要做的就是把影子系统里能被外部触发的路径全部列出来。我习惯把ATF的攻击面分成三类第一类是SMC系统调用从非安全世界任意EL都可以发起能触达PSCI、标准服务、平台自定义服务第二类是共享内存比如BL31与OP-TEE之间的共享buffer、与下一个启动阶段之间的参数传递结构体第三类是镜像加载路径也就是BL2解析FIP镜像、校验并拷贝的整个过程。审计时把握一个原则在每一个输入入口处是否验证了输入长度、地址范围、对齐要求在每一个镜像拷贝处是否有越界风险。6.2 加固检查清单与实际落地建议一个可以直接用的ATF加固检查清单大概是这样的确认platform_def.h里没有把EL3可写数据段映射成可执行确认开启的runtime services只包含实际需要的不需要的SDEI、SPMD就关掉确认BL31_BASE所在内存区域没有被Normal World的MMU映射掉确认TBBR的密钥存储方案里根私钥没有出现在固件包里确认最终发布的固件使用LOG_LEVEL_INFO以下级别。ATF历史上出过的不少安全问题都属于“内存越界写”或者“输入校验缺失”一类它们不会因为ATF是安全固件就自动免疫。安全固件的隐忧恰恰在于它极少被更新——很多设备量产之后ATF部分几乎不再升级。如果一款产品因为成本原因连TBBR都不开那至少要在启动链里加入版本号校验和回滚防护设计避免攻击者把设备降级到有漏洞的旧固件版本上。我个人的体会是ATF的代码质量和文档在开源固件里算得上第一梯队它的复杂不在于某个算法多难而在于它对平台细节的极度敏感。真正搞懂它需要的不是背概念而是亲手把QEMU平台编译一遍、烧录一遍、故意改坏一个地址再看它怎么崩溃。这篇文章里写到的调试路径、移植顺序和审计清单都是在这类“故意搞坏”的过程里试出来的。每个地址、每张页表、每次跳转背后都有值得较真的细节这也是我把这次源码评测整理成文的原因——做好EL3这层地基上层的高楼才值得信任。
返回列表