尧图精选

ATF源码级工程审计与平台移植实战:从BL31到安全启动链

🕒 发布时间:2026/9/4 10:58:34 📁 来源:尧图网络
1. ATF 这套安全固件到底在系统里扮演什么角色ARM Trusted FirmwareATF这套安全固件几乎是所有 ARM 64 位 SoC 里启动链路最底下的一层。我开始正式啃 ATF 源码是因为一块自研板卡上 BL31 老是起不来串口完全没有输出那时才真正意识到不开一个贴近源码的工程审计光靠改设备树和调 uboot根本没法解决这类问题。ATF 的全称是 Arm Trusted Firmware它不是一个普通意义上的 bootloader而是运行在 ARM 架构最高特权级 EL3 上的一套参考固件。它负责的事情主要有四块一是作为系统启动链上的信任根把后续镜像逐一认证进来二是在 EL3 里提供运行时服务例如 PSCI电源状态协调、系统电源管理、安全启动的告警上报三是作为 Secure World 和 Normal World 之间的门卫调度来自非安全侧的安全监控调用SMC四是为 TEE可信执行环境提供一个落脚的运行框架比配合 OP-TEE 工作时BL32 就是 TEE 内核所在的镜像。很多人第一次接触 ATF 时会被它的层级结构绕晕。简单类比整个 ARM 核心上有四个异常级EL0 到 EL3类似环形的不同权限等级。EL3 是最高的拥有对整个系统所有资源的最终控制权EL2 通常跑虚拟化层HypervisorEL1 跑操作系统内核EL0 跑应用程序。ATF 的核心镜像 BL31 就是那个长驻在 EL3 上的常驻固件它把自己的入口固定在某块受保护的 SRAM 或者 DRAM 区域系统启动之后永远不会被换页换掉也不允许 Normal World 侧的任何软件去改写除非通过它自己提供的明确接口。这个问题为什么值得专门做一次源码级评测因为 ATF 是整个系统里最底层、最敏感的那批代码。一旦 BL31 有漏洞或者被篡改上面无论是 Linux 内核、TrustZone 里的 TEE、还是安全支付、DRM、设备密钥管理任何依赖硬件信任链的安全机制都等于零。所以无论你是做嵌入式 BSP还是做安全设计评审或者只是想把 uboot 到内核的启动链路彻底搞明白ATF 源码都值得从头到尾梳理一遍。这篇文章我会从架构全景、源码工程审计、平台移植、以及我在实际板卡上踩过的坑这几个角度展开尽量把 ATF 这条线讲透让大家能直接照着落地。2. ATF 的整体架构与启动链路全景2.1 BL1、BL2、BL31、BL32、BL33五段式启动的职责划分ATF 的启动过程不是一个单一的大固件而是一组按顺序执行的镜像官方术语里叫 Boot Loader stage。每一段都有自己的使命并且后一段会验证前一段的合法性。完整路径是这样的BL1Boot ROM 固化代码或者由芯片厂家写入片内 ROM 的第一段它是整个信任链的起点也叫信任根。它负责初始化最小系统环境加载 BL2 并验证 BL2 的签名与哈希。BL2运行在 EL1 的 Trusted Boot Firmware负责从可信存储介质读取更多的镜像包括 BL31、BL32如果有、以及 BL33通常是 uboot 或 UEFI并且做完整性校验与认证。BL31运行在 EL3 的常驻运行时固件提供 PSCI、运行时服务、SMC 分发、以及 TEE 的进入退出上下文管理。它一旦运行起来就不再退出。BL32可选的 TEE OS 镜像例如 OP-TEE、Trusty跑在 Secure World 的 EL1。BL33Normal World 的入口也就是大家熟悉的 uboot、UEFI它最终负责引导 Linux 内核。很多资料会把 BL2 的 SCP_BL2、BL31 的 SCP_BL1 等额外镜像也混在一起但初学阶段先把 BL1 到 BL33 这条主线打通就够了额外的 SCP 固件属于电源管理域特有的东西不影响对整体链路理解。有个关键细节ATF 自身并不是必须完整地跑完这五段。在简化场景中某些平台会直接把 BL1 和 BL2 合并掉或者把 BL33 直接指向一个裸机程序。但在真实 SoC 上BL1 通常已经被芯片厂用硬件逻辑和 ROM 代码写死了能修改的部分在 BL2 之后。所以在做平台移植时我们关注的重点一般在 BL2 之后的镜像加载路径以及 BL31 的运行时行为。2.2 安全状态切换与运行时服务框架BL31 是 ATF 里最核心的一块它的运行时模型可以理解为一个“最高权限的服务大厅”。系统启动完成后Normal World 侧的 Linux 内核如果想要关机、重启、进入深度睡眠或者想要向 TEE 发起一个安全调用底层流程都是通过一条叫做 SMCSecure Monitor Call的特殊指令陷到 EL3然后由 BL31 来分发处理。在源码里这个分发机制的骨架是 runtime_svc_desc_t 结构体每种服务用 service id 和版本号做标识。例如 PSCI 服务占到一定 id 区间TEE 服务通过 SPDSecure Payload Dispatcher接入厂商自己的安全服务也可以通过注册一个新的服务描述符扩展进来。这种可插拔设计的思路和 Linux 内核里的子系统注册机制很像框架本身足够通用具体业务逻辑都挂在各自的 callbacks 上。这里还有一个容易理解错的概念Secure World 和 Normal World 以及 EL1/EL3 不是一回事。CPU 跑在 EL3 时就处于 Monitor 模式它是世界的仲裁者EL1 可以是 Secure 的 TEE也可以是 Normal 的 Linux靠的是 SCR_EL3.NS 位来区分。BL31 在切换世界之前要保存上一侧的寄存器上下文恢复另一侧的上下文这是它的主要开销来源也是很多调优工作的切入点。如果平台前后台频繁调用安全服务上下文切换的开销会直接影响性能这一点我在后面移植部分还会提到。2.3 镜像描述符与认证链TBBR 怎么保证启动可信ATF 对启动可信性的实现遵守 ARM 的 Trusted Board Boot RequirementsTBBR规范。它的思路不是简单地把所有镜像打包成一个整体而是在每个镜像外加一个证书或者签名块启动时逐级校验。在源码中编译时通过 TRUSTED_BOARD_BOOT1 选项打开认证功能然后每个镜像都会在加载前利用证书链和公钥信任根做验签。拿 BL2 来说BL1 用烧录在 eFuse 里的公钥哈希ROTPKRoot of Trust Public Key去验证 BL2 证书BL2 再用自己的私钥对 BL31、BL32、BL33 签发证书由此形成一个逐级认证的链条。这个流程里最常见的工程问题是密钥管理。开发阶段可以先用 build/.../keys 里自动生成的调试密钥一整套证书链通常在首次构建时自动生成但到了量产阶段必须换成芯片厂家和产品团队各自掌握的真实密钥。一旦私钥泄露整个信任链就形同虚设反过来如果私钥丢了后续 firmware 无法再签名产品只能原地报废所以量产密钥必须有多人备份和硬件加密卡保护。2.4 BL31 与 uboot、内核的配合关系结合热词里反复出现的“arm bl31 uboot”这里值得单独说清楚。很多开发者的板子上其实并不直接运行 ATF 的源码而是通过 uboot 的指令把 bl31.bin 加载到内存。问题往往出在入口地址和 uboot 的 TEXT_BASE 是否冲突或者 bl31.bin 需要对应平台特定的参数配置。正常配合流程是BootROM - BL1 - BL2 - BL31然后 BL31 把控制权交给 BL33也就是 ubootuboot 再引导内核。对于社区里的很多低价开发板人们习惯使用 Rockchip、Allwinner 或 Amlogic 的 vendor 分支并没有把 ATF 和 uboot 作为两个独立个体看待它们之间通过 ATF 编译时配置的平台参数或者设备树中的 firmware/scm 节点完成握手。对 uboot 而言它只需知道 BL31 的入口地址并且在跳转前准备好参数传递。我在实践中会发现一个非常典型的错误直接更新了 uboot 但忘了把 bl31.bin 同步升级导致新 uboot 传给 BL31 的参数格式不匹配系统卡在启动早期毫无输出。这种问题不是改代码能解决的而是必须在构建脚本里把 uboot、ATF、OPTEE、内核的 commit 号锁定成一个组合并把生成的烧写包做成版本一体的产物。这个经验对后续任何平台移植都适用。3. 源码工程审计目录结构、构建体系与安全关键点3.1 源码树怎么读lib、plat、bl1/bl2/bl31 的组成ATF 的源码根目录其实比很多人想象中简洁主要一级目录包括 bl1、bl2、bl31、plat、lib、include、drivers、tools、fdts 等。第一次进来的人容易陷进 plat 里一堆平台特有的代码完全忘记 ATF 上半身其实是一套体系结构通用代码。我的建议阅读顺序是先看 include/arch/arch_helpers.h 和 lib/el3_runtime/理解异常向量表和上下文切换怎么写的然后看 bl31/ 下的入口文件比如 bl31_main.c、runtime_svc.c、context_mgmt.c再回到 plat/your_platform/ 下看平台自定义实现。理解架构公共部分和平台部分的边界是源码审计的核心能力。平台相关代码通常集中在 plat/ 目录下每个平台一个目录例如 plat/arm/board/juno、plat/rockchip/rk3399、plat/allwinner 等。目录里最重要的文件是 platform.mk它定义了平台要编译哪些源文件、需要链接哪些库、设置 BL31 的入口地址、选择使用哪套串口驱动、有没有 TrustZone 配置等等。平台代码需要实现的接口在 include/plat/common/platform.h 里详尽列出比如 plat_setup、plat_get_bl31_params、plat_crash_console_init 等。所谓“平台移植”本质就是把这一组接口针对你的板子实现一遍。3.2 构建系统与关键编译选项ATF 的构建系统基于 GNU Make不支持 CMake也没有 Kconfig。官方支持的编译方式是在源码根目录下执行 make PLAT 加上一堆 build option。例如make PLATjuno \ DEBUG1 \ TRUSTED_BOARD_BOOT1 \ GENERATE_COT1 \ ARM_ARCH_MAJOR8 \ all这里的 PLAT 是核心必须指向 plat/ 下存在的平台目录名。DEBUG1 会把日志等级调成最高并保留符号表对调试很重要。GENERATE_COT1 和 TRUSTED_BOARD_BOOT1 配合时会在编译过程中调用 tools/cert_create 生成证书和密钥。如果要集成 OP-TEE还需要额外指定 SPDopteed让 BL31 把 OP-TEE 的镜像加载进来并注册对应的 SPD 服务。构建选项有个备忘录尽可能在调试阶段把所有安全启动相关选项打开哪怕你手里还没有真实密钥因为这样可以在开发早期就把证书链路径跑通避免到最后阶段才补功能。另外交叉编译工具链推荐使用最新的 aarch64-none-elf- 或 aarch64-linux-gnu-版本太老的话会出现链接器对某些重定位类型支持不全导致 BL31 链接失败。就我自己经验来说GCC 12 以上基本无痛旧工具链如果遇到 undefined reference 或 relocation truncated优先升级工具链再看代码问题。3.3 安全工程审计重点证书链、固件更新、异常路径从安全固件工程审计的角度应该关注的不只是启动能不能跑起来而是攻击面控制。基于源码我会重点扫这几个方向第一证书链和公钥处理。看 trusted_boot 代码里 ROTPK 的读取逻辑是直接编译进 BL1 的固定值还是从 eFuse 读取。如果固定值写在源码里那意味着只有通过重新流片或者改 BL1 ROM 才能更换根密钥这在量产审核时是一个重大决策点。第二固件更新流程。ATF 里对固件更新的支持通过 FWUFirmware Update模式体现这种模式允许系统进入一个最小化可信环境来升级所有后续镜像而不用依赖正常世界侧的 Linux。审计时我需要确认 FWU 模式下有没有多余的串口命令、有没有关闭认证的逃生舱防止生产环境被人拿到调试口之后直接绕过认证。第三异常路径与崩溃报告。ATF 的 crash_reporting 功能可以在 panic 时打印 CPU 寄存器现场。优秀的移植项目都会把 crash console 映射到某个固定串口这样即使主串口被安全侧占用也能通过另一路输出定位问题。缺少这个能力的平台遇到线上偶发崩溃基本只能抓瞎。下面的表格整理了审计中最常记录的信息点也适合作为一份出报告时的检查单审计点关注内容常见问题信任根存储ROTPK 来自 eFuse 还是代码常量开发密钥残余证书链证书格式、密钥强度、过期时间批次密钥未切换BL2 镜像加载支持从 flash/emmc/dram 加载介质地址与烧写工具不一致BL31 运行时PSCI 实现、中断路由、SMC 分发自定义服务未注册崩溃报告crash console 是否独立可用串口复用panic 无输出FWU 模式升级路径是否可认证调试命令暴露4. 平台移植落地从零把 ATF 跑到一块新板子上4.1 最小可行移植要改哪些文件新板子的 ATF 移植核心动作可以拆成三步给平台建目录、在 platform.mk 里声明编译参数、把所有平台接口用真实硬件实现填满。以一块用 Cortex-A72 的 SoC、串口挂在 UART0 上、地址 0xFF010000、BL31 运行在 0x1000 处为例我需要创建 plat/myboard/ 目录然后写 platform.mkPLAT : myboard BL31_SOURCES plat/myboard/myboard_bl31_setup.c \ plat/myboard/myboard_pm.c \ plat/myboard/myboard_topology.c接着解决链接脚本。ATF 每个 BL 镜像都有独立的链接脚本BL31 的链接脚本通常依赖平台定义的 BL31_BASE 宏。如果地址写错最常见的症状是 BL31 一跳到入口就触发 Data Abort。这个调试过程比较痛苦因为 PCB 上未必有 jtag串口在没有完成时钟和 pinmux 初始化之前也无法输出。所以最小可行移植还有一个重要原则第一版代码不要贪多先让串口出来一个字符才算地基打好。不要一上来就调 DDR、调电源管理、调 TEE那些都必须建立在线路初始化成功的基础上。4.2 让串口先响平台初始化与实际调试过程ATF 的串口输出不像 Linux 那样依赖设备树或驱动模型它是最简单的裸机轮询方式。在平台代码里你需要实现 plat_crash_console_init、plat_crash_console_putc、plat_crash_console_flush以及正常日志用的 console 初始化函数。常见的实现方式是用 PL011 或者 8250 驱动直接操作寄存器void myboard_console_init(void) { static const uart_params_t params { .uart_base 0xFF010000, .clock 24000000, .baud_rate 115200, }; console_pl011_register(0xFF010000, 24000000, 115200); }这里最容易踩的坑是时钟频率。很多移植版是从 datasheet 上拿了外设时钟值但板级 clk 在 BootROM 阶段已经被改过导致波特率算错输出全是乱码。我的习惯是先用逻辑分析仪测 UART TX 口的电平周期反推实际波特率然后再改代码。一旦串口通了BL31 的启动日志就能显示它在哪一步 panic。常见早期崩溃包括没有映射 MMU 导致访问非法地址、GIC 初始化失败导致中断无法路由、以及 DSU 调试接口没有关闭导致安全问题被 check 拦住。所有这些在启动日志上都会有显式提示前提是日志等级要开到 DEBUG。4.3 配合 BL33uboot跑通完整启动链当 BL31 能在反汇编级正常运行时下一步是把 BL33 引导起来。ATF 和 BL33 之间的握手协议依赖 BL2 在启动早期收集的参数块。参数块里记录着 BL33 的入口地址、CPU 启动方式、以及平台需要传递的 DTB 地址等信息。如果你是手工用 uboot 去加载 BL31而不是让 BL2 引导 BL33那流程会变成BootROM - BL1 - BL2 - BL31进入 BL31 后它检测到没有有效的 BL33 entry会回到等待状态然后 uboot 再接管最终跳进内核。这个模式下uboot 编译时需要使能 ARMv8 的 TF-A 支持知道 BL31 的地址并且识别 PSCI 接口。如果 uboot 里用了老的 spin-table 方式来启动多核而 ATF 的 PSCI 也开着两边会打架最典型的表现是开机只能跑一个核。推荐到这一步时做一次完整的自动化构建脚本把 BL31、BL33、设备树打包成一个 firmware 镜像再用烧录工具一次性写入。脚本里我一般会记录 commit 号、编译选项、密钥版本这样有问题时能快速还原现场环境。不要相信“我本地能跑”这句话要相信“一键构建产物”才能复现。4.4 移植完成后需要补的工程化能力很多移植到这一步就算完了但作为工程交付还不够。我还会做三件事第一给 BL31 增加 watchdog 喂狗逻辑防止安全固件死循环导致系统失去响应第二配置 TrustZone 地址空间控制器TZASC把安全内存和普通内存物理隔离开第三强制打开代码覆盖率构建和静态分析把 fortify 和 stack protector 加进去。ATF 其实本身就支持 stack protector 和 control flow integrity 相关的编译选项通过 ENABLE_STACK_PROTECTORstrong 和 ENABLE_PAUTH1 或者 BRANCH_PROTECTION1 打开。对于有安全需求的商用产品这些不是可选项而是合规项。如果读者做完基础移植后想提升代码健壮性我建议按这个顺序做先 STACK_PROTECTOR再开 crash reporting条件满足再上 pointer authentication。5. 我踩过的坑常见问题与排查技巧实录5.1 启动失败类串口无输出、BL31 panic我的板卡第一次跑 ATF搭了整整两天原因就是串口初始化函数里波特率算错了UART 输出全是乱码和“完全无输出”的现象几乎一样。后来我用示波器测 TX 脚波形才发现实际波特率比预期翻了一倍。如果你也有类似问题第一步永远是确认串口电平、波特率、时钟三个基本项再去怀疑代码。另一个高发问题是 BL31 panic 后系统直接挂死。ATF 默认的 panic 处理会尝试往 crash console 打印寄存器信息但如果 crash console 没有独立映射到可用的串口panic 时 CPU 直接进入 wfi 循环看起来就是死机。解决方式是提前配置好 crash console并保证这部分代码在 MMU 没有初始化时也能直接访问物理地址这样可以从 root cause 定位绝大多数早期问题。5.2 认证失败类证书、密钥与镜像批次不一致开了 TBBR 之后升级 bootloader 最容易踩的坑是证书和镜像批次不匹配。构建时会生成多个文件比如 bl31.bin、bl31.crt、nt_fw_key.crt 等。如果只是把 bl31.bin 和 uboot.bin 打包烧进去而没有同时更新证书BL2 验签时百分之百失败。我犯过的错误是开发时为了省事用了自动生成的 debug 密钥后面切换到正式密钥后没有把 BL1/BL2 一起重编。因为信任根在 BL1 里BL2 的签名必须用与 BL1 中烧录公钥对应的私钥签发。如果 BL1 没有更新新的 BL2 验不过如果同时更新了 BL1还需要确认 eFuse 里的 ROTPK 哈希是否还匹配。这个链条很容易扯皮最好的办法是写一个发布脚本把所有镜像、密钥、证书统一打包用版本号管理。5.3 移植类工程问题在移植中我发现设备树里的 psci 节点和 ATF 的 PSCI 版本不一致是特别隐蔽的问题。ATF 较新版本实现了 PSCI 1.1而旧内核与旧 uboot 可能只认 PSCI 0.2。某些情况下系统启动正常但执行 reboot 或者关闭某个 CPU 时会出现死循环或异常。排查方法是在内核 boot log 里看 psci 相关的 warning或者在 uboot 里执行 psci 查询命令验证版本。还有一类是 GIC 初始化的问题。ATF 的 BL31 会接管中断控制器将安全中断路由到 EL3普通中断路由到 Normal World。如果 GIC 驱动没有正确配置中断组可能会出现“Linux 能启动但是外设中断全部收不到”的诡异现象。这时候不要改内核先回 BL31 代码检查 GICD_IGROUPR 和 GICD_ISENABLER 配置再在 BL31 的 SMC 处理里加调试输出看中断有没有进来。5.4 独家建议与基本功练习最后分享一个我的建议ATF 的学习和移植不要一上来就碰高风险的真实板卡可以在 QEMU 的 virt 平台上先做交叉编译和仿真调试。使用 qemu-system-aarch64 -machine virt,secureon 可以直接运行 ATF 的 BL1/BL2/BL31搭配 uboot 和一个最小 ramdisk。通过 GDB 连接 QEMU可以一步步跟踪 BL31 的启动流程观察上下文切换。这一套流程成本最低最适合建立直觉。按照我的经验能把下面几个问题亲手做一遍ATF 就算入门了用 QEMU 跑通 BL31 并打印出 PSCI 版本修改平台配置让 BL31 日志等级从 NOTICE 变为 DEBUG在 BL31 里注册一个自定义 SMC 服务并返回固定值最后再把 TRUSTED_BOARD_BOOT 打开编译并烧写整个证书链。这些练习看着简单实际做下来你会对 EL3、SMC、GIC、TBBR 这些抽象概念有完全不同的理解后续无论做安全评审还是做平台适配心里都有底。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →