ARM Trusted Firmware深度解析:从EL3到BL31的平台移植实战
2026/9/6 11:08:55 网站建设 项目流程

说来也巧,我最早接触 Arm Trusted Firmware(后文统一叫 ATF,最近官方也叫 TF-A)是在一次板级 bring-up 的深夜。主控是某厂的四核 A53,DDR 跑通、 serial 也有 log 了,但一进 u-boot 就卡死。当时团队里没人对 EL3 有概念,我也以为 CPU 上电后从 BootROM 到 u-boot 是一条直线,结果折腾两天后发现,卡住的位置根本不在 u-boot,而在 BL31 往 BL33 跳转前的那个缝隙里。把 ATF 源码摊开读了一个周末后我才意识到,这颗芯片上电后真正干活的第一个“软件”,其实是 ATF,而 u-boot 只是被它请出来的客人。

这篇文章就围绕 ATF 的源码结构、安全模型、工程审计要点和平台移植流程展开,目标读者是正准备做 SoC bring-up、安全启动集成,或者仅仅是想搞懂“BL31 到底是什么”的底层固件工程师。我会把源码里那些绕口的宏定义和跳转逻辑尽量用白话拆开,同时把我在实际移植中踩过、修过的坑一并放出来。先给结论:ATF 并没有传说中的那么玄乎,但它的分层设计和安全假设,决定了你从拿到 SDK 到真正点亮内核的路径长度。

1. ATF 在系统启动链路里的真实位置

1.1 从 BootROM 到内核:ATF 插在哪一环

ARMv8/AArch64 架构下,SoC 从上电到内核跑起来的完整链条大概是这样:BootROM(固化在芯片里的只读代码)→ 某种 bootloader(可能叫 u-boot SPL、也可能叫 vendor 的 preloader)→ ATF → 主引导程序(主流是 u-boot)→ 内核。很多人一开始会搞混一个点:ATF 不是替代 u-boot,而是和 u-boot 协同工作,但它站在更高的特权级别上。

ARMv8 有四层异常等级:

  • EL0:用户态应用。
  • EL1:操作系统内核(Linux kernel 就跑在这)。
  • EL2:虚拟化管理程序(如果跑虚拟化的话)。
  • EL3:安全监视器(Secure Monitor),这是整个系统的最高权限层。

ATF 的几个镜像分别落在不同的异常等级上运行,但从全局看,它最重要的使命是“占据 EL3”,并且把 EL1/EL2 的入口地址通过 BL31 的设计暴露给下游。换句话说,u-boot 和 kernel 能运行的前提,是 BL31 先完成 EL3 下的初始化、并把世界切换(World Switch)的机制备好。

1.2 ATF 主要镜像:BL1、BL2、BL31、BL32

ATF 由多个镜像组成,每段镜像负责不同阶段:

  • BL1:固化逻辑的延伸,一般承载在 BootROM 里或由 BootROM 直接加载。它负责最早期安全初始化(比如设置 TrustZone 地址空间控制器、初始化 DRAM 控制器时序),然后加载 BL2。
  • BL2:运行在 EL1 Secure World,主要做 DDR 初始化之外的“平台配置汇总”,加载 BL31、BL32(如果存在)、BL33,并校验镜像签名(启用可信启动时)。
  • BL31:运行在 EL3,是 ATF 的 runtime 核心。它常驻内存,向内核/u-boot 提供 PSCI 电源管理调用、SIP 服务、Secure Monitor 调用入口。
  • BL32:可选的 TEE OS(比如 OP-TEE),运行在 S-EL1,通过 BL31 的调度与 Normal World 交互。

在实际的源码树里,没有整包 BL1/BL2/BL31 的独立目录树,而是通过编译选项(BL宏)决定当前构建哪个镜像。刚上手时如果没理解这个,很容易在 make 时困惑:为什么同一个plat/xxx目录能编出三个镜像?因为构建入口是按bl1.mkbl2.mkbl31.mk分别组织的,源码目录却是共享的。

1.3 BL31 为什么是 runtime 的核心

BL31 在完成初始化后不会“退出”,而是通过smc指令留在 EL3 等待调用。Normal World 的 u-boot 或 kernel 要关 CPU、要送 CPU 进 idle、要重启,都必须触发一个 SMC 异常,进入 EL3 让 BL31 代为处理。这种设计不是 ARM 闲得慌,而是安全上的硬性要求:如果允许 EL1 直接操作电源管理硬件,那一个被攻破的内核就能控制整台设备的上下电,Secure World 里的 TEE 和可信数据就毫无安全性可言了。

所以 BL31 本质上是一个常驻的、极小型的微内核,它只做几件事:处理异常、管理电源状态、转发安全服务请求。这话说起来很简单,但从代码审计角度,每一件事背后都是一长串平台相关回调。

2. 源码树与关键目录逐块拆解

2.1 顶层目录结构

ATF 源码的官方仓库是git://git.trustedfirmware.org/TF-A/trusted-firmware-a.git,也可以从 GitHub 镜像拉取。它的顶层目录大概如下:

bl1/ bl2/ bl31/ bl32/ plat/ lib/ drivers/ include/ fdts/ docs/ tools/

如果不带任何平台配置去看这些目录,很容易陷入“每个目录都像入口”的错觉。我的建议是:先忽略 bl1/bl2/bl31 的具体实现,从plat/入手。因为 ATF 的项目边界就是“平台适配层 + 通用框架”,通用框架的代码极其稳定,真正体现芯片和板卡差异的,全在plat/里。

2.2 plat 目录:平台移植的主战场

plat/下按厂商、SoC 组织,比如plat/arm/board/junoplat/rockchip/rk3399plat/nxp等。每个平台目录下通常包含:

  • platform.mk:定义平台源文件列表、编译选项、链接地址。
  • plat_common.c:平台公共函数实现,包括plat_get_next_bl_paramsplat_setup等。
  • plat_sip_svc.c:自定义的 SIP 服务,比如厂商私有的固件接口。
  • plat_pm.c:PSCI 相关回调,比如 CPU_ON、CPU_OFF、SUSPEND、SYSTEM_OFF、SYSTEM_RESET。
  • plat_topology.c:描述系统里有多少个 cluster、多少个 core。
  • plat_psci_common.c:PSCI 辅助函数。
  • include/platform_def.h:整个平台最核心的宏定义文件。这里定义了 BL31 的加载地址、MMU 配置、UART 基址、中断控制器基址等关键参数。

我见过很多平台 SDK 里默认带了一份 ATF,但 vendor 的代码和上游 ATF 往往存在版本滞后和大量私有 patch。做移植时,最好先对照docs/plat/下对应平台文档(若有),再结合platform_def.h去核对硬件地址是否和芯片 TRM 一致。这个看起来琐碎,却是移植第一天最值得花时间的环节。

2.3 lib 和 drivers:分清“框架”和“实现”

lib/里是架构相关的基础代码,比如lib/el3_runtime(EL3 运行时上下文切换)、lib/psci(PSCI 框架)、lib/xlat_tables_v2(MMU 页表操作)、lib/locks(自旋锁)。这些代码绝大多数情况下不需要改动,它们的作用是提供通用机制。

drivers/则按外设类型存放驱动,比如arm/gic*ti/uartio等。注意这里没有 Linux 内核里那种完整驱动模型,ATF 的驱动非常精简,往往只实现“够用”的寄存器操作。比如串口驱动只做输出,不做输入;GIC 驱动只做中断配置和分发,不管理复杂中断状态机。

从工程审计角度,最值得仔细看的是lib/psci/psci_main.clib/el3_runtime/aarch64/context_mgmt.c,前者是 PSCI 请求的分发核心,后者是世界切换的上下文保存与恢复。这两个文件几乎不随平台变化而变,理解了它们,你就掌握了 BL31 的主动脉。

2.4 BL31 入口:bl31_main 与 el3_entrypoint

很多人在源码里找 “main 函数”时一脸懵。BL31 的入口并不是传统意义的main,你会在bl31/bl31_main.c里看到bl31_main(),但它进入前还要经过一段冗长的汇编引导,代码在bl31/aarch64/bl31_entrypoint.S

一个简化的流程是:

  1. 汇编入口bl31_entrypoint设置异常向量表、初始化栈指针、记录当前 EL。
  2. 调用bl31_early_platform_setup(由平台层实现,初始化串口、GIC 等必要外设)。
  3. 调用bl31_plat_arch_setup(进入 MMU,映射 BL31 镜像和必要外设地址)。
  4. 跳到bl31_main,完成 runtime services 注册、PSCI 初始化、BL32 加载(若有)。
  5. 最后通过smc返回 Normal World,把控制权交给 BL33。

ES 这段流程里最容易出问题的是 MMU 配置。如果platform_def.h里的BL31_BASE和物理地址映射没有对上,BL31 一开 MMU 就 panic。真遇到这种问题,串口 log 往往只停在一行ERROR: ...或者直接一点输出都没有。

3. 安全模型与可信启动链

3.1 TrustZone、Secure World 与 Normal World

ARM TrustZone 把系统分为 Secure World 和 Normal World,两个世界各有完整的 EL3 → EL2 → EL1 → EL0 异常等级。硬件层面通过NS位(Non-Secure bit)标记总线事务,因此安全内存、安全外设可以做到物理隔离。ATF 作为 EL3 的软件,是这两个世界切换的唯一通道。

在代码里,最常见的体现是上下文结构体cpu_context_t:BL31 在收到 SMC 调用时,保存当前世界(比如 Normal World)的通用寄存器、系统寄存器到该上下文,再加载目标世界(比如 Secure World)的上下文。这个机制在lib/el3_runtime/aarch64/context_mgmt.c里体现得特别清晰。

3.2 可信启动链:从 BL1 到 BL33 逐级校验

启用 Trusted Board Boot(TBB)后,ATF 会对每个镜像做签名和哈希校验。ARM 官方提供了tools/cert_createtools/fiptool用于生成证书和打包 FIP 镜像。

简单理解:

  • BL1 校验 BL2 的证书和哈希。
  • BL2 校验 BL31/BL32/BL33 的证书和哈希。
  • 所有公钥的根(ROTPK,Root of Trust Public Key)烧在芯片 eFuse 或 OTP 里。

这套设计的安全前提是:根密钥必须在芯片出厂前安全烧录,且不能通过软件修改。实际项目里,如果你的产品想支持 OTA 固件升级,还需要额外引入 A/B 分区和防回滚计数器,ATF 本身并不负责完整的 OTA 策略。

3.3 安全固件审计的重点

从工程审计的角度,拿到一份 ATF 代码后,我会优先看这些东西:

  • platform_def.h里是否把安全世界的外设地址范围限制正确,有没有把安全 UART、安全 SRAM 的地址错映射到 Normal World 可访问区。
  • SMC 调用表和 SIP 服务的实现:厂商自定义的 SMC 往往是最容易出现漏洞的地方,比如参数校验不严、越界读写。
  • BL31 的栈空间分配:BL31 的栈非常小,平台默认通常只有几 KB 到十几 KB,如果有 bug 导致递归或超大局部变量,极容易栈溢出。
  • 是否打开了ENABLE_STACK_PROTECTOR这类编译保护选项。虽然 ATF 代码量不大,但在安全固件上,这类加固选项能开就开。

4. 平台移植实操:从零把 ATF 跑起来

4.1 移植前的准备

平台移植不是从零写代码,而是“改配置 + 补回调”。前提是你必须拿到以下材料:

  • SoC 的 TRM(Technical Reference Manual),尤其是内存映射、UART、GIC、Power 管理相关章节。
  • 当前 ATF 上游最新代码(或离你 SoC 最近的参考平台代码)。
  • 一块能跑通 vendor SDK 的板子,保证硬件本身没有问题。

如果芯片厂商提供了参考 ATF 代码,先对照参考代码与上游代码的差异,很多芯片支持是在上游合入的,直接拿上游分支做移植能省大量时间。

4.2 选择基准平台

ATF 的上游代码里有很多参考平台,最常见的是:

  • qemu:x86 本机模拟,适合学原理。
  • fvp:ARM 官方的 Fixed Virtual Platform,适合跑 Trusted Firmware 自带测试。
  • rockchip/rk3399mediateknxp等:适合挑一个和自己 SoC 结构最接近的做模板。

我的经验是:如果你的 SoC 也是“Cortex-A 系列 + GIC + PL011/8250 类 UART”,那就拿arm/board/fvp作为起点,再在它的基础上增加自己平台的目录。因为 FVP 平台对 ATF 新特性的支持最完善,源码里注释也最全。

4.3 建立平台目录的最小结构

假设我们要建一个名为myboard的平台,最小结构如下:

plat/myboard/ ├── platform.mk ├── plat_common.c ├── plat_topology.c ├── plat_psci.c ├── plat_sip_svc.c # 可选 └── include/ └── platform_def.h

platform.mk里至少需要声明:

PLAT_BL31_SOURCES += \ plat/myboard/plat_common.c \ plat/myboard/plat_topology.c \ plat/myboard/plat_psci.c PLAT_BL_COMMON_SOURCES += \ plat/myboard/plat_common.c

注意:ATF 的编译体系和普通 Makefile 略有不同,它用的是makePLAT=xxx的方式,进入make目录前要确保CROSS_COMPILE指向正确的 AArch64 交叉编译器。

4.4 关键宏定义:platform_def.h 要写对哪些东西

platform_def.h是移植第一步就要面对的硬骨头,最关键的几个宏如下:

#define PLATFORM_LINKER_FORMAT "elf64-littleaarch64" #define PLATFORM_LINKER_ARCH aarch64 #define PLATFORM_ADDR_SPACE_SIZE (1ull << 32) #define BL31_BASE 0x10000000 #define BL31_LIMIT 0x10020000 #define PLAT_PHY_ADDR_SPACE_SIZE (1ull << 32) #define PLAT_VIRT_ADDR_SPACE_SIZE (1ull << 32) #define PLAT_MMAP_ENTRIES 8 #define PLAT_MAX_XLAT_TABLES 3 #define PLATFORM_CORE_COUNT 4 #define PLATFORM_CLUSTER_COUNT 1 #define PLATFORM_MAX_CPUS_PER_CLUSTER 4 #define PLATFORM_MAX_CLUSTERS_PER_SYSTEM 1

这些值必须和芯片实际内存映射一致,尤其是BL31_BASE。在移植早期,最简单粗暴的方法是:把 BL31 放在 DDR 里的安全预留区,比如跑在 0x10000000,留足 128KB 或 256KB。有些 SoC 有专用的 SRAM,放在 SRAM 里更安全、性能也更好,但 SRAM 容量通常很小(几百 KB 以内),需要精确计算 BL31 镜像大小。

4.5 平台基础操作:串口、GIC、定时器

移植初期,最重要的是先把串口打通。ATF 的 log 全靠串口输出,串口不工作,后面所有调试都无从谈起。在plat_common.c里,你需要实现:

void plat_early_platform_setup(void) { console_16550_register(PLAT_UART_BASE, PLAT_UART_CLOCK_IN_HZ, PLAT_UART_BAUDRATE); }

如果你用的不是 16550 兼容串口,那就得去drivers/ti/uartdrivers/arm/pl011里找对应驱动。这里有个细节:BL31 的 log 在plat_early_platform_setup之前是输出不了的,所以如果串口完全没有输出,问题往往在更早的汇编阶段或链接地址配置。

GIC(通用中断控制器)的初始化也属于早期平台操作。ATF 需要配置 GIC 以接收来自安全世界和普通世界的中断,并将安全中断路由到 EL3。通常实现plat_secure_gic_initplat_secure_gic_pcpu_init两个函数。在没跑 TEE 的场景下,GIC 初始化不全会留下一个隐患:后面 Linux 内核启动时可能因中断配置异常而死机。

4.6 实现 PSCI 电源管理回调

BL31 向内核提供的电源管理服务来自 PSCI,而 PSCI 的回调需要平台层来实现。最常见的几个回调如下:

static const struct plat_psci_ops myboard_psci_ops = { .cpu_on = myboard_cpu_on, .cpu_off = myboard_cpu_off, .cpu_suspend = myboard_cpu_suspend, .cpu_resume = myboard_cpu_resume, .system_off = myboard_system_off, .system_reset = myboard_system_reset, }; int plat_setup_psci_ops(uintptr_t sec_entrypoint, const struct plat_psci_ops **psci_ops) { *psci_ops = &myboard_psci_ops; return 0; }

cpu_on回调的核心逻辑是:把次级 CPU(secondary core)的启动地址写到 SoC 指定的寄存器里,然后发送 CPU 启动事件(比如通过 SGI 中断或电源域控制器)。如果cpu_on实现不对,Linux 内核启动多核时会在CPU1: failed to boot之类的地方卡住。

早期调试时,可以暂时把cpu_off/suspend/system_off/reset实现成简单的死循环或直接返回,先把单核启动流程跑通。等主流程稳定了,再去逐个补全电源管理回调。

4.7 编译烧录与验证流程

编译命令示例:

make CROSS_COMPILE=aarch64-linux-gnu- PLAT=myboard BL33=../u-boot/u-boot.bin all fip

其中BL33指向你的 u-boot 镜像。ATF 构建完成后会生成build/myboard/release/bl31.binbuild/myboard/release/fip.bin。如果你的平台是从 BootROM 加载 BL31,那就直接用 BL31.bin;如果支持 FIP 格式,就用 fip.bin。

烧录后,通过串口应该能看到类似下面的输出:

NOTICE: BL31: v2.8(release): v2.8-631-g1234567 NOTICE: BL31: Built : 16:30:00, Mar 6 2025 INFO: ARM GICv3 driver initialized in EL3 INFO: BL31: Initializing runtime services INFO: BL31: Preparing for EL3 exit to normal world INFO: Entry point address = 0x200000

能走到最后一行,说明 EL3 世界切换已经成功,控制权即将交给 u-boot。如果卡在某一行之前,就回去检查对应环节的配置。

5. 常见问题与排查技巧实录

这段时间下来,我把碰到的典型问题总结成一份“速查表”,希望帮你少走弯路。

现象可能原因排查思路
串口完全无输出BL31 加载地址错误;UART 时钟/基址配置错误先确认 bootrom 是否真的加载了 BL31;用示波器/逻辑分析仪查 TX 脚波形;对照 TRM 查 UART 基址
串口输出一段后停止MMU 映射配置错误;GIC 配置导致 CPU 卡在异常检查platform_def.h的 MMAP 宏;关闭 GIC 初始化看能否执行到下一步
ERROR: Unsupported PSCI内核请求了平台未实现的 PSCI 功能编译 ATF 时打开 PSCI 相关调试宏;确认plat_psci_ops覆盖所有回调
多核启动失败cpu_on的启动地址寄存器写错;未正确发送 SGI读取 SoC 电源/启动控制寄存器,确认写值之后核心是否实际进入启动流程
BL31 报PANIC at PC最常见是栈溢出或非法地址访问用 JTAG 拿 PC 地址,对照 map 文件定位;调大 BL31 栈空间临时验证
启动 u-boot 后内核卡死BL31 返回时上下文恢复有误;GIC 配置被破坏比较 BL31 保存/恢复的上下文;检查 GICD_CTLR 和 GICC_CTLR 值

5.1 串口无输出的最坑场景:链接地址没对上

一次我拿到一块新板子,按参考平台的platform_def.h改了 UART 基址、时钟频率、波特率,编出来的 BL31 烧进去后串口一点动静都没有。查了半天,最后发现是 BootROM 加载 BL31 的地址是 0x10020000,但我在platform_def.h里写的BL31_BASE是 0x10000000。BL31 被加载到错的地址上,第一条指令执行就非法访问了,压根走不到串口初始化。

这种问题真要命的地方在于:bootrom 阶段没有 log,你不知道它到底把镜像搬到了哪里。后来我都是先在 TRM 里确认 bootrom 的加载地址,再回头定义BL31_BASE

5.2 开了 MMU 之后立刻挂掉

BL31 在bl31_plat_arch_setup之后会开启 MMU。如果页表配置不完整,遗漏了 BL31 镜像所在地址段,CPU 取指就会触发 Translation fault。现象是:串口 log 停在plat_arch_setup之前,然后没有任何输出。

排查时,先确认platform_def.hPLAT_MMAP_ENTRIES数量足够,再对照plat_common.cplat_get_mmap()返回的映射表,确保覆盖:

  • BL31 镜像的代码/数据段。
  • 串口等调试外设。
  • GIC 寄存器基址。
  • 其他运行时需要访问的外设。

5.3 BL31 与 u-boot 的地址冲突

BL31 常驻在内存中,不会退出。如果 BL31 加载地址和 u-boot 的加载地址重叠,最典型的现象是 u-boot 启动到一半(甚至刚加载)就死掉。这类问题往往不是 ATF 代码逻辑错误,而是地址规划不对。

我的习惯是:

  • BL31 放在 DDR 低地址或单独 SRAM 里,比如 0x10000000。
  • u-boot 放在 BL31 之后,比如 0x200000。
  • 内核加载地址、DTS 等都要避开 BL31 的驻留区域。

在 bring-up 阶段,宁可多预留一些空间,也不要让 BL31 和 u-boot 挤在一起。等整体链路稳定后,再按产品需求重新规划内存布局也不迟。

5.4 调试手段:LOG 级别与 FVP 仿真

如果板子上的硬件调试手段有限,强烈建议先用 ARM FVP 平台跑一遍 ATF,熟悉日志输出和代码流程。FVP 是软件仿真环境,支持 ARM 官方提供的固定虚拟平台模型,虽然不能完全替代真实硬件,但对学习 EL3 上下文切换、PSCI 调用路径非常有帮助。

在实际硬件上,经验是用make LOG_LEVEL=50编译,这会打开最详细的日志输出,ATF 会打印绝大部分函数调用路径。虽然 log 会慢一些、代码体积会大一些,但在 bring-up 阶段完全值得。等调试完了再回到LOG_LEVEL=40(INFO)或LOG_LEVEL=20(NOTICE)发布版本。

6. 一套能直接用的最小移植清单

如果你是第一次做 ATF 平台移植,给你一个可以直接照着做的清单:

  1. 拉取上游 ATF 代码,确认当前 master 分支的基本构建没问题(用PLAT=fvpPLAT=qemu)。
  2. 拷贝plat/arm/board/fvpplat/qemu的目录结构,改成自己的平台名。
  3. 对照 TRM 填写platform_def.h:内存基址、UART 基址、GIC 基址、CPU 拓扑、BL31 加载地址。
  4. 实现plat_early_platform_setup中的串口初始化,保证能从串口输出。
  5. 实现plat_setup_psci_ops,先只做system_offsystem_reset,其他回调直接返回错误,先跑通 BL31 → u-boot → kernel 单核启动。
  6. 增加cpu_on,验证多核启动。
  7. 加入 GIC 完整初始化,验证中断路由。
  8. 按需增加 SIP 服务、TEE 支持、Trusted Boot 签名校验。
  9. 压测:反复做rebootsuspend/resume、CPU hotplug,确认电源管理链路稳定。
  10. 收尾:把LOG_LEVEL调到发布级别,确认镜像大小满足存储分区约束。

这十条走下来,ATF 移植的地基就打牢了。剩下那些更复杂的(RAS、FF-A、SPM)都是在这个地基上的扩展,遇到具体需求再去啃对应 spec 也不迟。

我个人在实际操作中的体会是:ATF 的代码风格相当克制,抽象层级清晰,但正因为它“太简洁”,很多平台差异都藏在宏和回调节点里。移植时最忌讳照抄另一个平台,必须真正读懂每个宏被谁引用、每个回调被谁调用。把这个功夫下足了,后面接 TEE、接安全启动、做芯片 bring-up 都会顺很多。最后再分享一个小技巧:遇到不明不白的死机,先别急着调代码,用 JTAG 读一下当前 PCR 的值和cpu_context里的寄存器快照,往往比瞎猜代码快十倍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询