ARTICLE DETAIL

资讯详情

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

Arm Trusted Firmware架构解析:从BL31到平台移植的工程实践

Arm Trusted Firmware架构解析:从BL31到平台移植的工程实践 做嵌入式底层这一行的十有八九都绕不开这么个场景板子刚上电U-Boot 还没跑起来串口里打印的那几行字是谁输出的答案往往指向一段不起眼但极其关键的 EL3 固件——Arm Trusted Firmware。但如果你只把它当成“一段能初始化安全世界的裸机代码”那后面吃过的亏可能比想象的多得多。这篇评测我不会从概念讲起而是直接按源码工程的维度拆解 ATF 的完整架构以安全固件审计的视角过一遍信任链与密钥配置最后落回平台移植的实际操作把那些文档里不写、但实测必踩的细节一并交代清楚。这篇内容适合三类人一类是要在新 SoC 上移植 U-Boot 和 ATF 的 BSP 工程师一类是负责产品安全功能评审或固件合规的研发同学还有一类是刚开始接触 ARM64 启动流程、想搞明白 BL1/BL2/BL31 到底各自干什么的学生。1. ATF 源码工程的顶层拆解先看懂这颗“EL3 枢纽”长出哪些分支1.1 四级启动阶段与五类镜像文件的职责边界ATF 在 ARM64 系统中的角色可以概括为一句话它负责从复位向量到非安全世界引导加载程序之间的所有安全关键操作并给运行时的 TEETrusted Execution Environment提供入口。源码里也按照这个逻辑把启动过程拆得清清楚楚典型流程从 BL1 开始依次经过 BL2、BL31再根据产品是否需要可信执行环境来决定是否拉起 BL32最后跳转到 BL33。BL1 是系统复位后最早执行的镜像通常固化在片上 ROM 里厂家在流片时已经写死几乎不允许外部修改。它的任务非常有限初始化 CPU、设置必要的内存控制器状态、建立一条输出到串口的最小调试通道然后从 Boot 设备加载 BL2 并完成验签。BL2 由 BL1 加载运行在 EL3 之下、但又能访问安全内存的特殊位置它负责初始化 DRAM、加载并验证 BL31/BL32/BL33。BL31 则是整个 ATF runtime 的核心它常驻在 EL3 中提供 secure monitor、PSCI 电源管理、SMC 分发、可信 OS 交互等所有运行时能力。BL32 是可选的一般指 OP-TEE 这类可信操作系统BL33 就是大家最熟悉的 U-Boot、UEFI 或其它非安全引导程序。理解这一条链路的关键并不在于记住每个字母代表什么而在于看到每一级镜像之间的信任传递关系——前一级必须验证后一级后一级运行所需的资源由前一级提供。源码树中 bl1/bl2/bl31 三个目录正好对应这个过程加上 plat/ 下的平台代码和 drivers/auth 下的认证模块整个工程的分层设计可以说相当清爽。1.2 plat/ 目录的分层设计为什么厂商代码不能被塞进 common第一次翻 ATF 源码的人往往会在 plat/ 目录里迷路。这个目录下面既有 arm 这种官方参考平台又有 hisilicon、mediatek、qemu、rockchip 等一堆厂商目录。如果不理解它的分目录哲学后面移植大概率会越改越乱。ATF 的分层逻辑其实是common 代码描述“所有 ARM64 平台都满足的通用行为”platform 代码描述“某个具体 SoC 或板卡特有的行为”。比如平台要提供内存映射、要定义串口寄存器基址、要说明 GIC 如何配置、要给出电源域拓扑这些都属于 platform 层。而 BL31 的启动主流程、SMC 分发的框架、PSCI 服务的主干逻辑则属于 common 层原则上不应该被修改。反过来说官方在 common 层预留了大量回调接口就是为了让平台代码通过“填空”的方式接入。例如你在 plat/common/plat_psci_common.c 中会看到平台只需实现plat_get_sys_suspend_power_state等少数几个函数而完整的电源状态机已经由services/std_svc/psci/psci_main.c统一调度。这种写法的好处非常明显不同厂商之间的差异被隔离在各自目录里公共逻辑的 Bug 修复可以直接同步上游而不需要每个厂商单独合并。1.3 编译产物与三种真实加载方式ATF 的编译输出是典型的 ELF 与二进制分离字段bl31.elf、bl31.bin、bl2.bin、bl1.bin。但生产环境中这几份产物的组合方式远比 README 写的要灵活。第一种是完整 Trusted Board Boot 链路ROM Code → BL1 → BL2 → BL31 → BL33。这种组合对信任链要求严格BL1 需要被厂商固化在 ROM 中BL2 和 BL31 都要经过签名校验通常用于需要产品安全认证比如金融终端、车机的场景。第二种是 ROM Code 直接加载 BL31绕开 BL1/BL2。很多商用量产芯片采用这种方案因为片上 ROM 体积有限厂商直接让 ROM 读取烧录在 flash 中的 BL31 镜像并交给 CPU 执行。这种情况下BL31 需要自己完成原本 BL2 的部分初始化工作包括 DRAM 的简单配置和 GIC 初始化。第三种是把 BL31 打包进 U-Boot 的 FIT image由 U-Boot 加载后通过smc指令切入 EL3运行完 ATF 的初始化流程后再返回非安全世界。这种方案在调试板和低成本产品中很常见优点是能缩短启动时间但代价是 ATF 的信任根不再前置到复位向量安全强度有所下降。我自己的建议是如果产品不是特别在意防回滚与安全启动认证第三种方案性价比最高但一旦产品明确需要过安全评估就不要节省这一段信任链成本。2. BL31 运行时核心机制SMC 分发、PSCI 与中断路由的真实工作逻辑2.1 EL3 安全监控模式的 SMC 接口模型BL31 启动完成后CPU 停留在 EL3随时等待非安全世界通过smc指令发起调用。这条指令在 ARM64 汇编里就是一行smc #0但背后牵涉的上下文切换远比想象中复杂。CPU 在陷入 EL3 后硬件会自动保存一部分寄存器到 EL3 的异常栈中BL31 再通过异常向量表进入tspd、opteed或者std_svc这些运行时服务。服务的注册机制非常值得单独讲一讲。ATF 在runtime_svc_descs数组中维护一份“运行时服务描述表”每个服务通过DECLARE_RT_SVC宏声明。在services/std_svc/std_svc_setup.c里可以看到std_svc服务的rt_svc_descs定义它注册了 SMC call ID 高 8 位对应的标识。当smc指令过来时BL31 首先解析 call ID按区间匹配服务索引再调用该服务注册的 handler。实际做平台对接时你基本不需要改这套框架但必须知道两个关键点第一如果你要在自己的 ATF 里添加自定义 SMC 指令需要DECLARE_RT_SVC并选一个不冲突的 call ID 区间第二BL31 在处理 SMC 时会把标准调用转发给内核里的 EL3 固件接口即 Arm SMCCC而不会被内核误判。很多刚从 U-Boot 转过来的工程师在这里会卡很久因为他们在 U-Boot 里直接写裸机代码习惯了不习惯这种“先注册再分发”的框架式写法。2.2 PSCI 服务与 CPU 热插拔、系统挂起的落地细节PSCIPower State Coordination Interface是 ATF 最常被调用的标准服务之一kernel 里所有 cpu hotplug、cpuidle、系统休眠都需要通过 PSCI 指令落到 EL3 固件上。我以最常见的CPU_ON流程来拆解一下真正的执行步骤。内核调用psci_cpu_on后ATF 收到事件进入psci_cpu_on_start状态机。该函数会先在目标 CPU 的上下文信息中记录 entry point即该 CPU 被唤醒后要跳转的地址接着通过平台层的plat_setup_psci_ops接口拉起电源域。以 ARM Juno 平台为例实际动作是操作 SCPSystem Control Processor发消息等 SCP 完成上电后BL31 恢复目标 CPU 的异常向量栈引导它进入指定的 entry point最后执行eret返回非安全世界。这一整套流程里最容易被新平台忽略的是plat_psci_ops里cpu_standby、pwr_domain_on、pwr_domain_off这些回调函数是否都实现了。如果你的移植只实现了cpu_on而不实现cpu_off/cpu_suspend内核跑起cpuidle后系统会立即挂死而且报错链路非常难查——因为 CPU 进入 low-power 状态后串口中断可能已经丢了。实测下来先把psci_ops里每个回调都打上空实现或简单打印再逐步补齐真实电源管理逻辑会比一次性写完更快定位问题。2.3 中断路由模型FIQ 与 IRQ 如何在 EL3 转向安全世界BL31 不仅处理同步异常SMC也处理异步异常——中断。传统单核裸机思维下中断就是进了某个 ISR但到了 ATF 这个层级中断要按“安全/非安全”和“来源目标”来分类路由。ATF 定义了三种中断类型INTR_TYPE_S_EL1、INTR_TYPE_NS、INTR_TYPE_EL3。如果设备被配置为INTR_TYPE_S_EL1安全中断当它触发时GIC 会把中断以 FIQ 形式送进 EL3BL31 在handle_fiq里把它注入到当前运行的 TEE比如 OP-TEE中由 TEE 的安全中断处理程序处理处理完后返回到 EL3再由 BL31 返回到被抢占的上下文。如果配置为INTR_TYPE_NS则中断以普通 IRQ 注入到非安全世界的内核。平台移植时GIC 的配置通常放在plat_gic.c之类的文件中gicv3_driver_init会设置 GICD_CTLR 和 GICR_WAKER 等寄存器。常见的坑是 GICR_WAKER 的 ProcessorSleep 位没有正确清除导致中断永远进不来看起来像是 ATF 的 FIQ handler 没有工作实际连 GIC 都没完成唤醒。遇到这种情况先读GICR_WAKER寄存器确认处理器处于 awake 状态再往下查往往能省下好几个小时。3. 安全固件工程审计从源码角度核实 ATF 到底“锁”住了什么这一节涉及的审计角度全部基于对固件源码及其构建流程的检查不涉及任何攻击利用手法。以下检查项也能反向用于自查如果你是固件的开发或维护方这些就是你在交付前应该核实的内容。3.1 信任根与信任链的建立——BL1 为什么不能背锅安全启动的信任根Root of Trust是所有固件验签的起始点ATF 中信任根的位置不是 BL31更不是 BL33而是 BL1 及与其配套的芯片 ROM Code。这意味着如果攻击者能篡改 BL1 或者替换片上 ROM那么整条信任链的起点就已经被污染了。在 Trusted Board Boot 的实现中BL2 的镜像必须提供一张证书链证书里携带的镜像哈希值由 BL1 中的公钥验证。这里有个实践层面常被忽略的点BL1 的公钥不是在编译时从任意位置读取的而是从 E-fuse、OTP 这类一次性存储中获取或者从 BL1 镜像自身签名的附加数据中解析。做审计的时候需要检查固件构建环境里是否对这三类密钥做了严格隔离签发 BL31 的密钥、签发 BL33 的密钥、以及用于 BL2 证书的密钥是否在同一个目录、同一把私钥。安全实践中这些密钥应当分级管理不同镜像使用不同密钥并配置对应的反回滚计数器。如果想快速了解当前 ATF 的信任链配置可以直接看源码中的drivers/auth/auth_mod.c和drivers/auth/tbbr/tbbr_cot.c。其中tbbr_cot.c里定义了一张“证书链描述表”它描述了整个启动中各镜像文件的哈希计算顺序与校验方。通过读这张表可以清楚掌握某个镜像的签名验证到底依赖哪张证书、哪个公钥、哪个计数器。3.2 密钥格式、证书类型与防回滚设计ATF 的 TBBR 实现中证书格式总体遵循 X.509 v3使用cert_create工具生成默认采用 RSA 非对称算法。构建时TRUSTED_BOARD_BOOT1会触发这些证书生成流程同时编译出tb_fw_cert、soc_fw_cert、nt_fw_cert等文件。在审计时要特别注意平台是否真的使用了这些证书产物来验证固件还是只在编译时打了个-DTRUSTED_BOARD_BOOT1的标记实际烧录时完全没有做校验。这种“编译选项开启但启动链路没有启用验证”的情况在真实产品中出现过不少次。防回滚依赖 Non-Volatile CounterNVCounter。ATF 在plat/nxp/common、plat/arm/common这类平台代码中会有 NV 计数器实现常见策略是把计数器存储在 OTP 里每次升级时先校验新镜像的ctr值必须大于当前值再更新固件。如果不支持防回滚攻击者就可以把已经修复了漏洞的旧版本固件刷回去安全加固形同虚设。审计时我一般会按下面的清单逐项核对每项都能落到具体文件和配置上TRUSTED_BOARD_BOOT 编译开关是否为 1产物中是否包含证书文件。各镜像的签名密钥是否分离私钥是否存在于普通文件系统中。NV 计数器的实现是否连接到了 OTP 或 E-fuse而不是直接写 flash 上的固定偏移。BL31 的调试串口在 release 版本中是否被关闭ENABLE_DEBUG是否被置为 0。MMU 配置中安全内存区域的访问权限是否只读且不可执行是否存在冗余的可写可执行内存映射。3.3 拿到一份陌生 ATF 代码时的快速判读方法工程审计不太可能从头到尾读完全部源码更高效的做法是通过几个入口快速判断固件状态。第一步查看plat/目录下平台代码的改动时间线如果某个安全关键文件比如bl31_main.c被大量改动说明该平台的定制很重需要额外审查。第二步通过编译产物bl31.elf中的符号、版本字符串、调试符号残留来判定这是 debug 构建还是 release 构建。第三步用 binwalk 等二进制分析工具扫描固件镜像里的字符串和文件结构看是否包含调试命令、测试密钥等不该出现在产品中的内容。这些方法在拿到第三方厂商 ATF 代码时尤其有用。我见过不少项目的“安全固件”实际是厂商 SDK 里默认测试密钥构建出来的公钥和私钥都能在公开文档里找到。这种问题单看源码很难发现只有从密钥和构建配置层面做审计才能抓到。4. 平台移植落地从零开始让 ATF 在一块新板卡上跑起来4.1 新平台目录要准备的文件清单移植 ATF 的第一步是新建平台目录。以plat/myplatform/myboard为例需要准备的必备文件包括platform.mk、platform_def.h、plat_common.c、plat_topology.c、plat_psci.c。如果你的平台需要对接自定义电源管理还要有plat_sip_svc.c或plat_scmi.c这类扩展文件。platform.mk是构建入口控制源文件列表和链接脚本选择它会被顶层make系统自动引入。这里最需要注意的是平台宏的定义比如PLAT_PLAT_MAX_PWR_LVL、PLAT_NUM_PWR_DOMAINS这些值直接决定了 PSCI 状态机能管理的电源域数量配置不对会导致 cpu_topology 识别失败。platform_def.h中主要定义各镜像的基地址、大小、外设寄存器基址。以串口为例你需要把 UART 的寄存器基址填到PLAT_ARM_UART_BASE这类宏中并确认时钟频率与波特率参数匹配否则console_uart会因分频不准而打印乱码。4.2 串口通信与调试手段移植时先跑通的最小路径ATF 的移植调试中串口是第一优先级的调试通道。官方参考平台一般用 PL011 控制器你的平台可能是 8250 兼容串口就需要实现对应的console_putc、console_getc、console_flush。代码路径主要在drivers/arm/pl011/pl011_console.c或自定义驱动中。这里有一个经常被忽略的接口plat_crash_console。ATF 一旦在 EL3 陷入异常或者断言失败常规打印链路可能已经失效这时 BL31 会调用plat_crash_console输出崩溃信息。新移植平台如果只实现了常规 console 而没有实现 crash console系统 panic 后将完全静默排错会非常痛苦。移植时务必保证plat_crash_console在只有最基本 CPU 和串口寄存器初始化的情况下也能输出字符。另一个实用建议是先用 ARM 官方 FVPFixed Virtual Platform或 QEMU 做预移植验证不要在真实板卡上直接从零调。FVP 可以模拟 ATF 需要的大部分基础外设先在模拟器里跑通 BL31 启动再把代码切到真实硬件这样能隔离大量硬件初始化问题。4.3 内存视图与 MMU 配置ATF 为何只用一小段安全内存ATF 并不会像 U-Boot 那样把手头所有 RAM 都纳入管理。BL31 的安全内存被严格限定在BL31_BASE到BL31_BASE BL31_SIZE范围内这段内存通常位于 TZRAM/TZDRAM 区域既能被安全世界访问又被硬件属性限制为非安全世界不可访问。如果 BL31 在运行时要借助栈、堆、页表这些也都在该区域内分配。写平台移植时需要使用mmap_add_region()或者MAP_REGION_FLAT宏来定义页表。最常见的错误是页表的权限标志设置不正确比如把代码段标记成了可写可执行这会直接触发 ARM64 的 WXNWrite XOR Execute安全策略导致Permission fault。如果启动日志里出现ERROR: mmap_add_region_check() failed基本可以断定是新增的 mmap 区域与已有区域重叠或权限冲突需要回头检查平台定义的内存边界。4.4 避坑重点warm boot 入口和 reset handler 的处理平台移植中最容易翻车的往往不是首次冷启动而是二次启动。系统进入 suspend 后要恢复CPU hotplug 后要重新唤醒reboot 时要重新执行 platform 的 reset 流程这些全部依赖 warm boot 路径。plat_reset_handler在冷启动和 warm boot 时都会被调用但它拿到的 CPU 状态不同。如果平台代码在plat_reset_handler里做了 DDR 控制器重配置或 MMU 初始化而二次启动时这些硬件已经被设置为休眠态重复初始化就可能触发总线错误。正确做法是在plat_reset_handler中先通过某个复位原因寄存器判断当前是冷启动还是 warm boot再决定执行哪些初始化步骤。ARM Juno 平台在plat/arm/board/juno/plat_topology.c和电源管理文件里就有类似的判断可以参考。另一个容易踩的坑是 cache coherency。BL31 在 warm boot 早期访问共享内存时如果数据 cache 尚未使能且指令 cache 中残留旧数据会出现“代码在执行但读取到的全局变量是旧值”的诡异现象。解决方式是先执行dcache清理/无效化操作再访问共享结构体。这类问题在真实硬件上调试时经常被误判为内存不稳定实际上只是 cache 没有正确 maintain。4.5 启动成功判据与回归验证当 ATF 移植完成串口日志应该能看到类似下面的关键节点NOTICE: BL31: v2.8(release):v2.8-rc1-123-g12345abc NOTICE: BL31: Built : 09:12:00, Jan 18 2025 INFO: BL31: Initializing runtime services INFO: BL31: Preparing for EL3 exit to normal world INFO: Entry point address 0x82000000看到 “Preparing for EL3 exit to normal world” 只代表 BL31 本身的初始化完成并不代表整体启动成功。接下来还要看 U-Boot 是否接管、内核是否正常启动、cpu hotplug和suspend/resume是否回归通过。建议每次代码改动后都跑一遍固定的电源管理测试包括echo 0 /sys/devices/system/cpu/cpu1/online与echo 1 ...的 CPU 拔插操作以及systemctl suspend的休眠唤醒操作。这些测试覆盖的就是 BL31 PSCI 层最核心的代码路径。5. 工程实践层面的补充给正在移植或审计 ATF 的几点建议5.1 值得精读的几个源码文件如果你准备深入 ATF不用漫无目的地从顶层 Makefile 开始读。按照下面这份精简清单可以较快建立工程视角bl31/bl31_main.cBL31 的启动主流程能看到 runtime services 是如何逐个初始化的。services/std_svc/std_svc_setup.c标准服务注册与 SMC 分发的典型样例。services/std_svc/psci/psci_main.cPSCI 状态机主干架构最复杂的部分。plat/common/plat_psci_common.c平台需要填空的 PSCI 接口样例。drivers/auth/tbbr/tbbr_cot.c证书链配置理解安全启动的入口。5.2 验证工作不要只用一套工具链做 ATF 移植时建议不仅依赖板级串口调试还要结合 FVP 模拟器和第三方测试工具。比如 OP-TEE 环境下的xtest能覆盖大量 SMC 路径U-Boot 下的scp固件版本检查也能暴露 BL31 与 SCP 交互的兼容性问题。我见过不少团队只在一款板卡上验证通过就发布了固件结果换一批 DDR 颗粒或者换一个 GIC 版本休眠唤醒就出问题。所以有条件的话至少准备两套不同配置的硬件平台做交叉验证一套作为功能基线一套用于压力测试和低功耗验证。5.3 个人反复使用的几条经验最后分享几条从实际项目中沉淀下来的经验每一条都对应过真实的线上问题第一编译 ATF 时不要随手打开ENABLE_DEBUG这样不仅增大镜像体积还会在 release 固件中留下大量调试信息给安全审计增加不必要的风险。第二定制 SMC 服务时务必为每个调佣编号单独写一个 handler 函数而不是在一个switch里堆全部逻辑否则后期维护和审计都很难下手。第三在平台代码中保留一份固定的启动日志格式包括 ATF 版本、构建时间、平台名称等基础信息这对线上问题回溯非常有帮助。很多设备出问题后厂商连固件版本都无法从日志里判断往往就是少了这些基本信息。写这篇评测之前我特意重新打开源码树把启动流程、PSCI 状态机和 TBBR 证书链过了一遍仍然能从一些边角逻辑里找到新收获。ATF 这套工程代码真正的价值在于它把安全启动、电源管理、运行时服务这些底层事务沉淀成了一组可扩展的框架而不仅仅是某一块板卡的启动补丁。希望这篇拆解能帮你在移植或审计 ATF 的时候少走几步弯路。
返回列表