ARM Trusted Firmware (ATF) 入门:安全启动与可信执行环境实战指南
1. 项目概述:从零开始理解ARM安全世界的基石
如果你正在接触基于ARM架构的嵌入式系统,尤其是那些涉及安全启动、可信执行环境(TEE)或者系统安全加固的项目,那么“ARM Trusted Firmware”(简称ATF)这个名字你一定绕不过去。我第一次接触ATF时,面对其庞大的代码仓库和复杂的初始化流程,也是一头雾水。它不像我们熟悉的U-Boot那样有大量现成的板级支持包(BSP),也不像简单的裸机程序那样直观。ATF更像是一个隐藏在系统深处的“安全管家”,在操作系统和应用程序启动之前,就默默地构建起了一道坚固的安全防线。
简单来说,ARM Trusted Firmware是ARM公司为基于ARMv8-A及后续架构(包括部分ARMv7-A)的处理器,提供的一套开源参考实现,用于实现系统的安全启动和运行时安全服务。它定义了从芯片上电复位到将控制权交给非安全世界(如普通操作系统)的整个可信启动链。这个项目标题“ARM ATF入门-安全固件软件介绍和代码运行”,精准地指向了学习ATF的两个核心阶段:首先是理解它是什么、为什么需要它;其次是能够动手搭建环境,让代码真正跑起来,看到效果。这对于任何想深入ARM平台底层安全,或者从事相关固件开发的工程师来说,都是必须跨越的第一道门槛。
2. ATF的核心定位与安全启动模型解析
2.1 为什么需要ATF?——ARM安全扩展的必然产物
在传统的嵌入式系统或早期的ARM系统中,启动流程相对简单:Boot ROM -> Bootloader(如U-Boot)-> 操作系统。然而,随着移动支付、数字版权保护、企业级安全等需求的爆炸式增长,这种简单的启动链变得不堪一击。恶意代码可能在启动早期就被植入,从而掌控整个系统。
ARM公司从ARMv7-A架构开始引入了TrustZone技术,这是一种硬件级别的安全方案,它将处理器的工作状态划分为两个“世界”:安全世界(Secure World)和非安全世界(Normal World)。你可以把它想象成一栋大楼里的“金库区”和“公共办公区”。操作系统和应用大部分时间运行在“公共办公区”(非安全世界),而涉及密钥、指纹、支付等敏感操作,则在硬件隔离的“金库区”(安全世界)进行。
但是,光有硬件隔离(金库)还不够,你必须确保从大楼通电的那一刻起,通往金库的道路就是绝对安全、未被篡改的。这就是安全启动(Secure Boot)要解决的问题。ATF正是这套安全启动流程的软件实现框架。它定义了一个分阶段的启动模型,通常被称为BL1, BL2, BL31, BL32, BL33。
2.2 ATF启动阶段深度拆解
理解这几个阶段是读懂ATF代码的关键。它们像一场精密的接力赛,每一棒都有明确的职责和交接条件。
BL1 - 信任根(Root of Trust)这是安全启动链的绝对起点,通常是芯片内部ROM中的代码,不可修改。它的核心职责只有两个:验证BL2镜像的完整性和真实性(通过数字签名),然后跳转到BL2。BL1本身是硬件信任的延伸。
BL2 - 可信引导加载程序(Trusted Boot Firmware)BL2通常加载到芯片内部的SRAM中运行。它的任务更重一些:
- 初始化必要的基础硬件,如更复杂的时钟、关键外设控制器。
- 从外部存储(如eMMC、SD卡)加载后续阶段的镜像,包括BL31、BL32(可选)、BL33(通常是U-Boot)。
- 验证这些镜像的签名。
- 将控制权交给BL31。
BL31 - 运行时固件(Runtime Firmware)这是ATF的核心,是一个常驻内存的“安全监视器”。它运行在EL3(ARMv8-A的最高特权异常等级)。它的核心功能是管理“世界切换”。当非安全世界的操作系统(BL33)需要访问安全世界的服务(BL32)时,会产生一个“安全监控调用(SMC)”,此时CPU会陷入EL3,由BL31来处理这个请求,并安全地切换到BL32。你可以把BL31看作是大楼里那个掌管“金库区”和“办公区”唯一通道的、绝对中立的保安。
BL32 - 安全世界操作系统(可选)这就是运行在安全世界的软件,比如OP-TEE(一个开源的TEE实现)。它提供具体的安全服务,如加密、安全存储、指纹验证等。BL31负责在它和BL33之间安全地传递请求和结果。
BL33 - 非安全世界软件通常就是我们所熟悉的引导加载程序,如U-Boot。它运行在非安全世界,负责最终加载Linux内核等操作系统。从ATF的角度看,BL33已经是“不可信”的普通软件了。
注意:在实际项目中,BL1和BL2有时会被芯片厂商的Boot ROM或第一级Bootloader替代。ATF代码通常从作为BL31开始编译和集成。但理解完整的链条对于调试和解决启动问题至关重要。
3. 开发环境搭建与代码获取
3.1 工具链的选择与配置
要让ATF跑起来,第一步是准备交叉编译工具链。ATF主要使用ARM架构的aarch64-none-elf-或aarch64-linux-gnu-工具链。我强烈建议使用Linaro官方发布或ARM官方提供的GCC工具链,兼容性最好。
以Ubuntu系统为例,安装aarch64-linux-gnu工具链:
sudo apt update sudo apt install gcc-aarch64-linux-gnu安装后,通过aarch64-linux-gnu-gcc -v验证是否安装成功。
如果你需要编译用于模拟器(如QEMU)的、不带操作系统依赖的纯固件,可能需要aarch64-none-elf-工具链。可以从ARM开发者网站或xPack项目页面下载预编译版本,并添加到PATH环境变量中。
3.2 获取ATF源代码与依赖
ATF的源代码托管在GitHub上。使用Git克隆主分支:
git clone https://github.com/ARM-software/arm-trusted-firmware.git cd arm-trusted-firmwareATF本身依赖不多,但为了构建一个完整的可启动系统,你通常还需要其他组件:
- U-Boot:作为BL33。
- Linux Kernel:最终要启动的系统。
- (可选)OP-TEE:如果你想运行完整的TEE示例。
一个高效的作法是为你的实验项目创建一个独立的工作目录,用Git分别管理这些仓库。
3.3 构建系统的理解:Makefile与平台定义
ATF使用Makefile作为构建系统。其核心编译命令格式如下:
make CROSS_COMPILE=<toolchain_prefix> PLAT=<platform> <target>CROSS_COMPILE: 指定交叉编译工具链前缀,如aarch64-linux-gnu-。PLAT: 指定目标平台。这是关键参数。ATF支持众多平台,如:qemu:用于QEMU虚拟化平台,是学习和调试的最佳起点。fvp:用于ARM Fixed Virtual Platform,功能强大的官方仿真模型。rpi3、rpi4:树莓派3/4,是常见的实体硬件实验平台。sun50i_a64:全志A64芯片平台。
<target>:构建目标,最常用的是all(编译所有)和bl31(仅编译BL31)。
平台相关的代码位于plat/目录下。每个平台子目录(如plat/qemu/)里都包含该平台特定的初始化代码、内存布局定义(platform.mk)和链接脚本。当你指定PLAT=qemu时,构建系统就会去链接这个目录下的文件。
4. 在QEMU上运行ATF:首个“Hello World”
4.1 编译用于QEMU的ATF镜像
QEMU是一个功能强大的开源模拟器,它完美模拟了一个ARMv8-A的“虚拟开发板”,并且ATF官方对其支持非常完善。这是我们进行代码阅读、单步调试和功能验证的理想沙盒。
首先,确保你已安装QEMU的系统模拟组件:
sudo apt install qemu-system-arm进入ATF源码目录,为QEMU平台编译一个完整的固件镜像(包含BL1、BL2、BL31等):
make CROSS_COMPILE=aarch64-linux-gnu- PLAT=qemu all编译成功后,你会在build/qemu/release/目录下找到关键输出文件,最重要的是bl1.bin和fip.bin。
bl1.bin: 对应启动阶段的BL1镜像。fip.bin: 这是一个“Firmware Image Package”,它由fiptool工具创建,内部打包了BL2、BL31、BL33(U-Boot)等镜像。这是ATF启动后期加载的核心文件。
4.2 整合U-Boot作为BL33
ATF需要跳转到一个非安全世界的软件(BL33)。我们选择U-Boot作为这个BL33。
获取并编译U-Boot:
git clone https://github.com/u-boot/u-boot.git cd u-boot # 为QEMU的ARMv8(Cortex-A57)目标配置并编译 make qemu_arm64_defconfig make CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)编译后得到
u-boot.bin。创建FIP包: 回到ATF目录,使用其自带的
tools/fiptool/fiptool来创建或更新FIP包,将U-Boot集成进去。# 假设u-boot.bin在../u-boot目录下 tools/fiptool/fiptool create \ --tb-fw build/qemu/release/bl2.bin \ --soc-fw build/qemu/release/bl31.bin \ --nt-fw ../u-boot/u-boot.bin \ build/qemu/release/fip.bin这个命令创建了一个新的
fip.bin,其中包含了BL2 (bl2.bin)、BL31 (bl31.bin) 和非可信固件BL33 (u-boot.bin)。
4.3 使用QEMU启动并观察日志
现在,我们可以用QEMU命令来启动这个完整的软件栈:
qemu-system-aarch64 \ -machine virt,virtualization=on,gic-version=3 \ -cpu cortex-a57 \ -nographic \ -smp 1 \ -m 1024 \ -kernel ./build/qemu/release/bl1.bin \ -device loader,file=./build/qemu/release/fip.bin,addr=0x4000000 \ -d in_asm,unimp参数解析:
-machine virt:指定使用QEMU的“virt”虚拟机器,这是ARMv8的通用虚拟平台。-cpu cortex-a57:指定CPU模型。-nographic:无图形界面,使用控制台。-kernel ./build/qemu/release/bl1.bin:将BL1作为“内核”加载到QEMU模拟的ROM地址。-device loader...:将FIP包加载到指定的物理内存地址(0x4000000),这个地址需要与ATF代码中plat/qemu/include/platform_def.h里定义的PLAT_QEMU_FIP_BASE一致。-d in_asm,unimp:输出一些调试信息,有助于观察执行流。
如果一切顺利,你将看到串口输出滚过ATF各个阶段的启动日志,最终进入U-Boot的命令行界面。看到U-Boot的提示符,就证明ATF成功完成了安全启动链,并将控制权移交给了BL33。
实操心得:第一次运行时,最常见的失败原因是内存地址不对齐或FIP包内容错误。务必确认
PLAT_QEMU_FIP_BASE的地址与QEMU命令中的addr=参数完全一致。另一个技巧是,可以先不加-d参数,只保留-nographic来观察纯净的启动日志,定位问题阶段。
5. ATF代码结构导读与关键流程分析
5.1 源码目录结构剖析
进入ATF源码根目录,其结构清晰反映了其模块化设计:
bl1/,bl2/,bl31/:各启动阶段的专属源代码。plat/:平台移植层。这是将ATF适配到具体硬件芯片的关键目录。你需要关注的平台代码都在这里,例如plat/qemu/。include/:全局头文件,定义公共API和数据结构。lib/:可复用的库文件,如加密库、标准C库替代品、CPU辅助函数等。drivers/:通用设备驱动,如控制台、定时器、中断控制器(GIC)的抽象层。services/:运行时服务,如电源状态控制(PSCI)、安全监控调用(SMC)处理框架。tools/:构建和镜像处理工具,如前面用到的fiptool。
对于移植和深度定制,plat/和include/drivers是你需要花费最多时间的地方。
5.2 BL31初始化流程详解
BL31作为常驻的运行时固件,其初始化流程是ATF的核心。让我们跟踪bl31/aarch64/bl31_entrypoint.S这个汇编入口点:
- 冷启动路径(Cold Boot):CPU从EL3开始执行BL31的入口函数
bl31_entrypoint。 - 设置异常向量表:首先配置EL3的异常向量表,以便处理来自低异常等级(EL2, EL1)的同步/异步异常、SMC调用等。
- CPU数据初始化:初始化每个CPU核心的上下文数据,为多核启动做准备。
- 控制权移交C代码:在完成最底层的汇编环境设置后,跳转到C语言函数
bl31_main(位于bl31/bl31_main.c)。 - bl31_main 核心工作:
- 平台早期初始化:调用
plat_early_platform_setup(),让平台代码初始化最关键的硬件,如串口用于打印调试信息。 - 运行时服务初始化:调用
runtime_svc_init(),这是BL31的“灵魂”。它遍历一个名为runtime_svc_descs的数组,这个数组登记了所有在EL3提供的“服务”。每个服务都有一个唯一的SMC功能号(Function ID)和对应的处理函数。- 最重要的服务包括:标准服务(如PSCI电源管理)、安全监控服务(处理与TEE的通信)等。
- 架构和平台后期初始化:进行更细致的硬件配置。
- 准备进入下一阶段:最终,BL31会根据预定的启动计划,决定是跳转到安全世界的BL32(如OP-TEE),还是直接跳转到非安全世界的BL33(U-Boot)。它通过
bl31_prepare_next_image_entry()和bl31_plat_runtime_setup()来设置好目标世界的CPU上下文(寄存器状态、异常等级等)。
- 平台早期初始化:调用
- 世界切换:BL31执行一条
ERET(异常返回)指令。这条指令不会返回到调用点,而是根据它设置好的CPU上下文,让CPU“跳转”并切换到目标世界(EL2或EL1,安全或非安全状态)去执行。至此,BL31的初始化完成,进入等待SMC调用的服务状态。
5.3 SMC调用处理流程实战
当非安全世界的Linux内核或U-Boot需要请求安全服务(例如,调用一个加密算法)时,它会执行一条SMC指令。这会触发一个异常,CPU陷入EL3,并跳转到BL31设置的异常向量表。
- 异常向量表路由:汇编代码保存当前CPU上下文(寄存器)后,会路由到C处理函数
smc_handler()(通常在lib/aarch64/runtime_exceptions.S和runtime_svc.c中)。 - 服务查找:
smc_handler会解析SMC指令携带的功能号(Function ID)。这个ID是一个32位或64位的数,其中包含了服务类型(是快速调用还是标准调用)、调用实体(是安全世界还是非安全世界发起)以及具体的服务编号。 - 服务派发:根据功能号,在
runtime_svc_descs数组中查找注册的处理函数。 - 执行与返回:调用对应的服务处理函数。函数执行完毕后,BL31会恢复之前保存的非安全世界上下文,再次执行
ERET,返回到非安全世界的调用点,并携带返回值。
这个过程就像应用程序通过操作系统API(系统调用)请求内核服务一样,只不过这里是通过硬件指令SMC和固件BL31,在安全世界和非安全世界之间进行切换和服务调用。
6. 移植ATF到新硬件平台的关键步骤
将ATF运行在QEMU上只是第一步。真正的挑战是将它移植到一块真实的、新的ARMv8-A开发板上。这通常涉及以下核心步骤:
6.1 创建平台目录与基础文件
在plat/目录下为你平台创建一个新目录,例如plat/my_company/my_platform/。你需要创建或移植以下关键文件:
platform.mk:构建系统的核心。定义平台属性。# 示例片段 PLAT_MY_PLATFORM := 1 ENABLE_PIE := 0 # 是否支持位置无关代码 BL31_SOURCES += plat/my_company/my_platform/my_platform_setup.c \ plat/my_company/my_platform/my_platform_pm.c这里需要指定本平台特有的源文件,并覆盖一些全局编译选项。
include/platform_def.h:硬件抽象的核心。定义平台特定的内存布局和硬件常量。#define PLATFORM_STACK_SIZE 0x1000 // 栈大小 #define BL31_BASE 0x80000000 // BL31加载地址 #define BL31_LIMIT (BL31_BASE + 0x20000) // BL31内存范围 #define PLAT_MY_PLATFORM_UART_BASE 0x12345000 // 串口寄存器基地址 #define PLAT_MY_PLATFORM_GICD_BASE 0x12346000 // GIC Distributor基地址 #define PLAT_MY_PLATFORM_GICR_BASE 0x12347000 // GIC Redistributor基地址这些地址必须严格参照你的芯片数据手册(Datasheet)或硬件参考手册。
6.2 实现平台初始化函数
ATF通过一组强制的平台接口函数来适配硬件。你需要在C文件中实现它们:
控制台初始化:这是调试的命脉。实现
plat_my_platform_console_init(),初始化串口硬件,并调用console_register()注册到ATF的通用控制台框架。void plat_my_platform_console_init(void) { /* 1. 配置串口引脚复用、时钟 */ /* 2. 设置串口波特率、数据格式 */ /* 3. 注册 console_t 结构体 */ static console_t my_console; int ret = console_register(PLAT_MY_PLATFORM_UART_BASE, PLAT_MY_PLATFORM_UART_CLOCK, PLAT_MY_PLATFORM_UART_BAUDRATE, &my_console); if (ret == 0) { /* 注册成功 */ } }系统计数器初始化:ATF的延时和调度依赖系统计数器。实现
plat_my_platform_syscnt_freq()返回计数器频率(如24MHz)。中断控制器(GIC)初始化:这是多核和SMC处理的基础。实现
plat_my_platform_gic_init(),配置GIC Distributor和CPU Interface。代码通常可参考其他平台,但基地址必须修改正确。电源管理(PSCI)操作:如果支持多核,需要实现PSCI相关的平台函数,如
plat_my_platform_get_core_pos()(通过MPIDR获取核心索引)、plat_my_platform_pwr_domain_on()(启动从核)等。
6.3 配置链接脚本与内存映射
plat/my_company/my_platform/linker.ld.S文件定义了BL31等镜像在内存中的布局(代码段.text、数据段.data、BSS段.bss的起始地址)。这些地址必须与platform_def.h中的定义以及你的Bootloader(BL2)加载地址相匹配,且不能与其他软件(如U-Boot、Linux内核)的内存区域冲突。
6.4 集成与构建测试
- 在ATF根目录的
make_helpers/plat_helpers.mk中,添加你的平台到ALL_PLATFORMS列表。 - 尝试编译:
make CROSS_COMPILE=aarch64-linux-gnu- PLAT=my_platform bl31。 - 将生成的
bl31.bin与你为这块开发板定制的BL2和BL33(U-Boot)打包成FIP或符合你芯片启动格式的镜像。 - 通过JTAG/SWD调试器或BootROM提供的下载工具,将镜像烧录到开发板,并通过串口观察输出。
踩坑实录:移植中最常见的问题是“死机”,串口无任何输出。排查顺序应是:1)串口引脚和时钟是否正确?用示波器量一下TX引脚。2)内存地址是否正确?BL31的加载地址是否在可执行的内存范围内(如SRAM或初始化好的DDR)?3)栈指针(SP)在早期汇编代码中是否设置到了有效内存?4)异常向量表地址是否正确?可以通过在汇编入口点放置一个死循环,并用调试器单步来确认代码是否被执行。
7. 调试技巧与常见问题排查
7.1 利用日志与断言
ATF内置了多级日志系统(ERROR,WARN,INFO,VERBOSE)。在include/common/debug.h中,可以通过修改LOG_LEVEL宏来调整输出级别。在开发初期,建议设置为LOG_LEVEL_INFO甚至LOG_LEVEL_VERBOSE以获取更多信息。
断言assert()在ATF中广泛使用。当条件为假时,它会打印断言失败信息并挂起CPU。这是定位程序逻辑错误的利器。确保在调试版本中启用断言。
7.2 使用调试器(JTAG/SWD)
对于实体硬件,调试器是必不可少的。以J-Link为例,配合GDB进行调试:
- 编译时在Make命令中加入
DEBUG=1,生成带调试信息的ELF文件(如build/my_platform/debug/bl31/bl31.elf)。 - 在链接脚本中确保代码段从正确的内存地址开始。
- 通过J-Link GDB Server连接开发板,并用arm-none-eabi-gdb加载ELF文件。
- 在关键函数(如
bl31_main,plat_my_platform_console_init)设置断点,单步执行,查看寄存器值和内存内容。
7.3 常见启动问题速查表
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 串口无任何输出 | 1. 串口硬件/引脚初始化错误 2. BL1/BL2未正确加载或跳转 3. 栈指针(SP)设置错误导致立即崩溃 4. 代码未在预期地址执行 | 1. 检查时钟、引脚复用配置,用示波器测TX。 2. 用调试器停在最早汇编代码,检查PC和SP。 3. 在汇编入口点写一个简单循环(如点亮LED),验证代码是否运行。 |
| 打印若干条日志后死机 | 1. 内存访问越界(如访问未初始化的DDR) 2. 中断配置错误导致异常 3. 数据或代码段地址与链接脚本不符 | 1. 检查死机前最后一条日志附近的代码。 2. 检查GIC初始化代码,确认中断号、优先级配置。 3. 对比map文件(编译生成)中的地址与实际加载地址。 |
| 无法跳转到U-Boot (BL33) | 1. BL33加载地址或入口地址错误 2. BL31为BL33设置的CPU上下文(如SCTLR, ELR)错误 3. U-Boot镜像本身损坏或格式不对 | 1. 检查plat_my_platform_get_bl33_mem_params()返回的参数。2. 在BL31跳转前( bl31_prepare_next_image_entry)打印或调试查看为BL33设置的上下文结构体entry_point_info_t。3. 单独验证U-Boot镜像是否能被之前的Bootloader直接启动。 |
| SMC调用无响应或返回错误 | 1. SMC功能号未在BL31中注册 2. SMC处理函数本身有bug 3. 非安全世界调用SMC的姿势不对(参数传递) | 1. 在BL31的runtime_svc_init中检查服务描述符数组。2. 在SMC处理函数入口添加日志,确认是否被调用。 3. 核对ARM架构手册关于SMC指令和参数寄存器的约定(X0-X7)。 |
7.4 模拟器调试进阶:结合GDB与QEMU
对于QEMU,可以方便地使用GDB进行源码级调试,这对理解ATF执行流有巨大帮助。
- 在编译ATF和U-Boot时,都加上
DEBUG=1选项。 - 启动QEMU,并添加
-s -S参数。-S表示启动时暂停CPU,-s是-gdb tcp::1234的简写,表示在1234端口等待GDB连接。qemu-system-aarch64 -machine virt ... -s -S -nographic ... - 打开另一个终端,启动GDB,并连接到QEMU:
当执行到aarch64-linux-gnu-gdb ./build/qemu/debug/bl31/bl31.elf (gdb) target remote localhost:1234 (gdb) break bl31_main # 在BL31的C入口点设置断点 (gdb) continue # 让QEMU继续执行bl31_main时,QEMU会暂停,GDB会获得控制权,此时你就可以单步执行、查看变量、回溯调用栈了。
这个过程能让你清晰地看到BL1如何验证并跳转到BL2,BL2如何加载FIP包,BL31如何初始化并最终跳转到U-Boot。这种动态的、可视化的理解,是单纯阅读代码无法比拟的。
移植和调试ATF是一个系统工程,需要耐心地对照手册、分析代码、利用工具。每一次成功启动,都意味着你对ARM安全体系的理解又深入了一层。这个“安全管家”虽然隐藏在深处,但正是它,为上层丰富多彩的应用世界奠定了可信的基石。
