1. 项目概述ARM ATF到底是什么如果你在嵌入式开发特别是基于ARMv8-A架构的芯片上折腾过启动流程那么“ATF”这个词你大概率不会陌生。它全称是ARM Trusted Firmware中文常被叫做ARM可信固件。我第一次接触它是在为一个客户调试一块高性能网络处理器时系统启动到某个阶段就卡住了串口日志里反复出现“BL31”的字样当时就一头雾水。后来才知道这就是ATF的核心组件之一。简单来说ATF是ARM官方提供的一套开源固件它定义了从CPU上电复位到将控制权交给上层操作系统比如Linux之间整个安全启动链条的软件实现框架。它解决的是一个在复杂多核系统里“谁先启动怎么启动如何保证安全”的根本问题。在传统的简单嵌入式系统里可能一段Bootloader如U-Boot就直接把内核拉起来了。但在现代ARMv8-A及以后的服务器、移动和物联网芯片里引入了“安全世界”和“非安全世界”的硬件隔离概念这是ARM TrustZone技术启动过程变得异常复杂。ATF就是用来管理这个复杂过程的“总调度员”和“安全基石”。它不是一个你可以直接运行的用户程序而是一套需要编译进芯片ROM或Flash在系统最底层运行的固件代码。理解ATF对于从事底层系统开发、安全启动方案设计甚至是解决某些诡异的启动故障都至关重要。2. ATF的核心架构与组件拆解ATF的代码组织非常清晰它采用了一种分阶段启动的模型每个阶段承担不同的职责像接力赛一样传递控制权。这套模型是理解ATF如何运作的关键。2.1 启动阶段详解从BL1到BL31的接力ATF将启动流程划分为几个明确的阶段通常我们称之为BL1, BL2, BL31, BL32, BL33。这个编号大致代表了它们的执行顺序。BL1 - 可信启动ROM代码这是冷启动后CPU最先执行的代码。在大多数芯片设计中BL1是固化在芯片内部ROM只读存储器里的你无法修改。它的职责非常基础但关键初始化最核心的CPU和必要的片上硬件比如用于加载下一阶段代码的存储控制器建立一个极简的运行环境然后从外部存储设备如eMMC、SPI NOR Flash中加载并验证BL2的镜像。这里的关键词是“验证”。BL1内部会包含一个或多个公钥它使用非对称加密算法通常是RSA或ECDSA来验证BL2镜像的数字签名。只有验证通过才会跳转到BL2执行否则启动失败。这构成了安全启动的第一道防线。BL2 - 可信启动加载器BL2通常是我们开发者可以定制和修改的第一个阶段。它运行在SRAM静态随机存储器中。BL2的任务更重一些它会初始化更丰富的硬件外设如DDR内存控制器将DDR内存配置好然后从存储设备中加载后续所有的固件组件包括BL31、BL32可选和BL33。同样BL2在加载每个组件前也会对其进行验签。此外BL2还负责解析一个重要的配置文件——FIPFirmware Image Package。FIP就像一个压缩包把BL31、BL32、BL33等镜像以及它们的证书打包在一起方便管理和加载。BL31 - 运行时服务固件EL3固件这是ATF的核心也是我们平时打交道最多的部分。BL31运行在ARM特权等级最高的EL3Exception Level 3。你可以把它理解为一个常驻的、特权极高的“安全监控器”或“系统服务管家”。它的核心功能包括世界切换管理“安全世界”Secure World和“非安全世界”Normal World之间的切换。当非安全世界的操作系统如Linux需要调用某个安全服务时比如加解密它会触发一个特殊的指令smcCPU陷入EL3由BL31接管判断请求后可能将执行流转到安全世界的BL32去处理处理完再切回来。电源状态管理接口PSCI为操作系统提供标准的CPU热插拔、挂起/唤醒、系统关机等电源管理功能。Linux内核通过smc指令调用BL31提供的PSCI服务来实现这些功能。安全监控器调用SMC调度作为所有SMC请求的总入口和调度中心。BL31通常常驻在系统内存DDR中不会被覆盖。BL32 - 可信操作系统可选BL32运行在安全世界的EL1或EL0它是一个独立的、小型的可信执行环境TEE操作系统比如OP-TEE。它负责处理具体的、复杂的可信应用TA比如指纹识别、数字版权管理DRM、移动支付等。BL31作为“管家”负责把来自非安全世界的服务请求“派发”给BL32。如果你的产品不需要复杂的TEE功能可以省略BL32。BL33 - 非安全世界软件这就是我们熟悉的“正常世界”的软件通常是U-Boot这类Bootloader或者直接是Linux内核。BL31在完成自身和BL32如果有的初始化后会将CPU的执行等级从EL3降低到EL2或EL1并将控制权交给BL33的入口点。从此系统就进入了我们熟悉的Bootloader或内核启动流程。2.2 关键概念异常等级EL与安全状态要理解ATF必须搞清楚ARMv8的异常等级EL0-EL3和安全状态Secure/Non-secure。EL0用户态运行普通应用程序。EL1内核态运行操作系统内核如Linux内核或TEE内核如OP-TEE内核。EL2虚拟机监控程序态运行Hypervisor用于虚拟化。EL3最高特权级运行安全监控器固件即我们的BL31。只有EL3的代码才能改变CPU的安全状态。安全状态由EL3固件控制。CPU可以处于“安全世界”或“非安全世界”。安全世界可以访问所有内存和外设资源而非安全世界的访问会受到限制。BL31/32运行在安全世界BL33如Linux运行在非安全世界。ATF的BL31驻留在EL3它像一扇门的守卫控制着两个世界之间的通道通过SMC指令和这扇门的开关安全状态切换。3. 环境准备与代码获取在开始动手编译和运行ATF之前我们需要一个合适的开发环境。ATF是高度硬件相关的所以你必须明确你的目标平台。3.1 硬件与软件依赖硬件平台选择对于学习和入门强烈建议使用模拟器。最理想的选择是ARM官方提供的固定虚拟平台FVPFixed Virtual Platform。FVP可以完美模拟一个包含Cortex-A系列CPU、内存、外设的虚拟硬件环境并且完全支持ATF的启动流程和TrustZone。你可以在x86的Linux开发机上直接运行无需任何实体开发板。ARM会为不同CPU模型如Cortex-A57x4, Cortex-A76x4等提供对应的FVP二进制包。如果你有实体开发板那当然更好但请务必确认该板卡的芯片厂商提供了ATF的移植支持。通常芯片厂商如NXP、TI、瑞芯微等会维护自己芯片平台的ATF移植代码你需要在他们的SDK或Git仓库里找到。软件环境搭建Linux开发机推荐使用Ubuntu 20.04 LTS或更新版本。大部分操作在终端完成。交叉编译工具链ATF主要用C和汇编编写。你需要ARM架构的交叉编译器。对于AArch6464位ARM工具链前缀通常是aarch64-none-elf-或aarch64-linux-gnu-。可以从ARM官网或Linaro网站下载。# 例如安装Linaro的交叉编译器 sudo apt-get install gcc-aarch64-linux-gnu构建工具ATF使用make作为构建系统。确保已安装。sudo apt-get install make设备树编译器DTCATF和U-Boot等组件使用设备树Device Tree来描述硬件。需要安装device-tree-compiler。sudo apt-get install device-tree-compilerFVP模拟器从ARM官网下载与你目标CPU模型对应的FVP包并解压。将其路径加入系统PATH环境变量。3.2 获取ATF源代码ATF的源代码托管在GitHub上使用git克隆即可。git clone https://github.com/ARM-software/arm-trusted-firmware.git cd arm-trusted-firmware建议切换到一个稳定的发布分支比如v2.9避免使用最新的开发主干分支以减少不确定性。git checkout v2.9代码目录结构清晰docs/官方文档非常重要入门必读。include/公共头文件。lib/通用库函数如加解密、字符串操作。drivers/通用驱动框架。plat/平台相关代码。这是移植和适配不同芯片的关键目录。你会看到plat/arm/ARM参考设计、plat/nxp/、plat/ti/等子目录。services/运行时服务实现如PSCI、SPM安全分区管理器。bl1/,bl2/,bl31/,bl32/各启动阶段的源代码。注意初次接触时不要被庞大的代码量吓到。我们初期只关注编译和运行流程不会深入每一行代码。重点理解plat/下的平台端口和构建配置。4. 编译与运行以FVP平台为例让我们以ARM的FVP_Base_RevC-2xAEMvA一个模拟双核Cortex-A72的平台为例演示最基础的ATF编译和运行流程。这个流程是理解ATF实际运作的绝佳起点。4.1 基础编译流程ATF的编译通过向make命令传递一系列参数来完成。最基本的编译命令需要指定目标平台PLAT、编译工具链CROSS_COMPILE和构建类型DEBUG或RELEASE。# 在ATF源码根目录下执行 make PLATfvp \ CROSS_COMPILEaarch64-none-elf- \ DEBUG1 \ all fip让我们拆解这个命令PLATfvp指定目标平台为fvp。这告诉构建系统去plat/arm/board/fvp/目录下寻找平台特定的配置和代码。CROSS_COMPILEaarch64-none-elf-指定交叉编译工具链的前缀。make会使用aarch64-none-elf-gcc、aarch64-none-elf-ld等工具。DEBUG1启用调试符号和优化等级-O0方便后续用调试器如GDB跟踪代码。生成产品固件时应使用DEBUG0。all fipall目标是编译出各个BL镜像bl1.bin, bl2.bin, bl31.bin。fip目标则是使用fiptool工具将这些独立的镜像以及可选的BL32、BL33镜像打包成一个fip.bin文件这个文件就是最终要被烧录或加载的固件包。编译成功后你会在build/fvp/debug/目录下找到关键输出文件bl1.bin: BL1镜像注意对于FVP这个BL1是ATF提供的模拟版本真实芯片的BL1在ROM里。bl2.bin: BL2镜像。bl31.bin: BL31镜像。fip.bin: 集成了BL2、BL31等内容的固件包。4.2 构建完整的启动链集成BL33U-BootATF自己并不能直接启动Linux它需要将控制权交给一个BL33。最常用的BL33就是U-Boot。因此我们需要先编译一个U-Boot然后将其作为BL33集成到FIP包中。1. 编译U-Bootgit clone https://github.com/u-boot/u-boot.git cd u-boot # 为FVP平台配置并编译U-Boot。qemu-arm64_defconfig是一个适用于64位ARM虚拟平台的通用配置。 make CROSS_COMPILEaarch64-none-elf- qemu-arm64_defconfig make CROSS_COMPILEaarch64-none-elf- -j$(nproc)编译完成后在U-Boot根目录会生成u-boot.bin文件这就是我们的BL33镜像。2. 重新编译ATF并集成U-Boot回到ATF源码目录我们需要在编译命令中指定BL33的路径。make PLATfvp \ CROSS_COMPILEaarch64-none-elf- \ DEBUG1 \ BL33/path/to/your/u-boot/u-boot.bin \ all fip这次编译fiptool会自动将u-boot.bin作为BL33打包进fip.bin。4.3 使用FVP模拟器运行现在我们有了包含BL2、BL31和BL33U-Boot的fip.bin。FVP模拟器需要一个“闪存镜像”来模拟板载存储。通常我们把bl1.bin和fip.bin拼接起来形成一个完整的闪存镜像。# 在ATF编译输出目录下操作 cd build/fvp/debug/ # 将bl1.bin和fip.bin拼接成flash.bin。bl1.bin需要放在flash的起始地址0x0。 cat bl1.bin fip.bin flash.bin接下来启动FVP模拟器。FVP命令参数复杂但一个最小化的启动命令如下FVP_Base_RevC-2xAEMvA \ --data cluster0.cpu0bl1.bin0x0 \ # 将bl1.bin加载到CPU0的地址0x0模拟ROM --data cluster0.cpu0flash.bin0x08000000 \ # 将flash.bin加载到地址0x08000000模拟Flash -C bp.flashloader0.fnameflash.bin \ # 指定Flash加载器文件关键 -C bp.secureflashloader.fnamebl1.bin \ # 指定安全Flash加载器文件关键 -C bp.ve_sysregs.exit_on_shutdown1 \ # 关机时退出模拟器 -C cache_state_modelled0 \ # 关闭缓存状态建模加速模拟 --console # 启用控制台输出实操心得FVP的参数非常灵活也容易出错。--data参数用于在启动前将文件加载到指定内存地址常用于加载内核或设备树。而-C bp.flashloader0.fname和-C bp.secureflashloader.fname这两个参数是模拟硬件启动流程的关键它们告诉FVP用哪个文件来模拟物理Flash芯片的内容ATF的BL1会从这里去加载BL2和FIP。参数配置错误会导致启动流程断在奇怪的地方。如果一切顺利你会在控制台看到类似以下的启动日志这清晰地展示了ATF的启动接力过程NOTICE: BL1: v2.9(release):v2.9 NOTICE: BL1: Built : 00:00:00, Jan 01 2023 INFO: BL1: RAM 0x4000000 - 0x4008000 INFO: Loading image id5 at address 0x4000000 ... NOTICE: BL2: v2.9(release):v2.9 NOTICE: BL2: Built : 00:00:00, Jan 01 2023 INFO: BL2: Doing platform setup INFO: Loading image id3 at address 0x80000000 ... NOTICE: BL31: v2.9(release):v2.9 NOTICE: BL31: Built : 00:00:00, Jan 01 2023 INFO: BL31: Initializing runtime services INFO: BL31: Preparing for EL3 exit to normal world INFO: Entry point address 0x80000000 INFO: SPSR 0x3c9 ... U-Boot 2024.01 (Jan 01 2023 - 00:00:00 0000) DRAM: 2 GiB ... 看到U-Boot的命令行提示符恭喜你这说明ATF已经成功完成了它的使命安全地初始化系统并将控制权移交给了BL33U-Boot。你可以继续在U-Boot中加载Linux内核和根文件系统完成整个系统的启动。5. 代码导读与关键流程分析仅仅能运行还不够我们得知道ATF的代码是如何组织并实现上述流程的。我们以BL31为例因为它是最核心的运行时服务组件。5.1 BL31的入口与初始化BL31的入口点定义在bl31/aarch64/bl31_entrypoint.S这个汇编文件中。CPU从BL2跳转过来后首先执行的就是这里的bl31_entrypoint函数。它的工作主要是设置异常向量表runtime_exceptions这样当发生中断、SMC调用时CPU知道跳转到哪里处理。从BL2传递过来的参数中获取重要的信息比如BL32和BL33的入口地址、内存布局等。初始化CPU数据cpu_data和系统寄存器。跳转到C语言的主函数bl31_main。bl31_main函数位于bl31/bl31_main.c这是BL31初始化的核心void bl31_main(void) { /* 1. 早期平台初始化 */ bl31_early_platform_setup(); /* 2. 初始化运行时服务框架 */ runtime_svc_init(); /* 3. 平台后期初始化 */ bl31_platform_setup(); /* 4. 初始化BL32如果存在 */ bl31_prepare_next_image_entry(); /* 5. 如果BL32存在则跳转到BL32否则直接准备进入BL33 */ ... }其中runtime_svc_init()函数至关重要。它会遍历一个名为runtime_svc_descs的数组这个数组里注册了所有在EL3提供的运行时服务比如标准服务std_svc包含PSCI功能、spmd_svc用于SPMC模型等。每个服务都定义了自己的处理函数handle当非安全世界发起一个SMC调用时BL31会根据SMC的功能号Function ID路由到对应的服务处理函数。5.2 SMC处理流程剖析SMCSecure Monitor Call是非安全世界与安全世界EL3通信的桥梁。当Linux内核或U-Boot需要执行PSCI操作如关闭CPU时就会发起一个SMC调用。假设一个简单的PSCI调用流程在Linux内核中调用psci_cpu_off()函数。该函数会准备参数并执行smc汇编指令陷入EL3。CPU跳转到BL31设置的异常向量表中的smc_handler在bl31/aarch64/runtime_exceptions.S中。smc_handler会保存当前CPU上下文寄存器状态然后调用C函数smc_handler在bl31/bl31_main.c中。C处理函数解析SMC的功能号。PSCI的功能号有一个特定的范围例如0x84000000-0x8400FFFF。根据功能号找到注册的std_svc服务并调用其对应的处理函数std_svc_smc_handler在services/std_svc/std_svc_setup.c中。std_svc_smc_handler进一步解析子功能号如PSCI_CPU_OFF调用真正的PSCI实现函数psci_cpu_off()在lib/psci/psci_main.c中。PSCI函数执行具体的关核操作操作GIC中断控制器、发送核间中断等。执行完毕后逐级返回最后由异常向量表的代码恢复CPU上下文并通过eret指令返回到非安全世界的调用点。这个过程清晰地展示了BL31作为“调度中心”和“服务提供者”的角色。所有从非安全世界发起的敏感操作都必须经过EL3的审查和处理。5.3 平台移植关键点要让ATF在一个新的芯片平台上运行主要工作集中在plat/目录下。你需要为你的平台创建一个子目录例如plat/my_company/my_platform/。其中必须实现几个关键接口平台初始化函数在plat_my_platform.c中实现plat_my_platform_init()用于初始化该平台特有的硬件如时钟、串口、内存控制器等。内存布局定义在plat_my_platform.h中定义PLAT_ARM_BLx_BASE和PLAT_ARM_BLx_LIMIT等宏明确告诉ATFBL1、BL2、BL31等镜像应该被加载到内存的什么位置。这个布局必须和你的链接脚本linker.lds以及BootROM的加载约定保持一致。控制台串口驱动实现console_my_platform_register()注册一个用于打印调试信息的串口驱动。这是调试的“眼睛”没有它启动过程就是黑盒。电源管理操作实现plat_my_platform_system_reset()和plat_my_platform_system_off()等函数提供系统重启和关机的底层操作。这些会被PSCI服务调用。平台特定的Makefile在平台目录下创建platform.mk定义该平台的编译选项、源码文件列表等。注意事项平台移植最棘手的部分往往是内存布局和缓存一致性Cache Coherency问题。错误的内存布局会导致镜像加载到错误地址直接跑飞。而在多核环境下一个核修改了内存数据另一个核可能因为缓存看不到最新数据导致诡异的问题。ATF提供了flush_dcache_range()等API来处理缓存在操作共享数据结构如PSCI状态时必须小心。6. 调试技巧与常见问题排查调试ATF这种底层固件和调试上层应用完全不同。它没有成熟的OS环境打印日志和利用调试器是主要手段。6.1 日志输出与调试信息ATF使用一个分等级的日志系统ERROR,WARN,INFO,VERBOSE。默认编译DEBUG1会打开INFO及以上级别。你可以在代码中使用INFO(Loading image id%d\n, image_id);来打印信息。确保你的平台控制台驱动已正确实现并注册否则看不到任何输出。在make命令中可以通过LOG_LEVEL参数调整日志级别。例如LOG_LEVEL50会打开VERBOSE级别的详细日志对追踪启动流程细节非常有帮助但输出量巨大。make PLATfvp LOG_LEVEL50 DEBUG1 all fip6.2 使用调试器GDB连接FVP这是最强大的调试手段。FVP支持通过GDB的远程调试协议gdbserver进行连接。首先以允许调试的模式启动FVP并指定一个端口FVP_Base_RevC-2xAEMvA \ ... (其他参数同上) ... --cadi-server port7100 \ # 开启CADI服务器用于性能分析等非必需 -C bp.vis.disable_visualisation1 \ # 关闭图形界面减少开销 -C bp.vis.rate_limit-enable0 \ --no-vis \ -C bp.pl011_uart0.out_fileuart0.log \ # 将串口0输出重定向到文件 -C bp.pl011_uart0.unbuffered_output1 \ --gdbserver port9000 \ # 在9000端口启动gdbserver --parameter cluster0.cpu0.semihosting-enable1 \ --parameter cluster0.cpu0.semihosting-heap_base0x40000000 \ --parameter cluster0.cpu0.semihosting-heap_limit0x1000000然后在另一个终端使用交叉编译工具链中的GDB进行连接aarch64-none-elf-gdb build/fvp/debug/bl31/bl31.elf # 加载带调试符号的BL31镜像 (gdb) target remote localhost:9000 # 连接到FVP (gdb) break bl31_main # 在BL31的main函数处设置断点 (gdb) continue # 继续执行当启动流程执行到bl31_main时FVP会暂停GDB会获得控制权。此时你可以单步执行step/next、查看变量print、查看寄存器info registers、查看内存x命令甚至修改内存值。这对于分析复杂的启动死机问题比如在某个函数里卡住是必不可少的。6.3 常见启动问题与排查思路启动失败时串口日志是唯一的线索。以下是一些典型场景问题一启动卡在“BL1: ...”之后没有任何BL2的日志。可能原因1BL1加载或验证BL2失败。排查检查编译生成的bl2.bin大小是否超出了BL1中定义的加载区域限制BL2_BASE和BL2_LIMIT。检查bl2.bin是否被正确打包进了fip.bin使用fiptool info fip.bin查看。在FVP中确认-C bp.flashloader0.fname参数指向了正确的、包含BL2的flash.bin或fip.bin文件。可能原因2BL2的入口地址设置错误。排查检查plat/fvp/include/platform_def.h中BL2_BASE的定义并确保链接脚本bl2/bl2.ld.S中的入口地址_start与之匹配。问题二BL31打印初始化日志后系统挂起没有进入BL33U-Boot。可能原因1BL33U-Boot镜像加载地址或入口点错误。排查检查传递给make的BL33参数路径是否正确镜像是否有效。在BL31的日志中寻找类似Entry point address 0x...的信息确认这个地址是否是U-Boot镜像的实际入口地址通常是CONFIG_SYS_TEXT_BASE。可以使用aarch64-none-elf-objdump -f u-boot查看U-Boot的入口点。可能原因2CPU模式或安全状态切换错误。排查BL31在跳转到BL33前需要将CPU从EL3切换到EL2或EL1并从安全世界切换到非安全世界。调试时可以在bl31_prepare_next_image_entry函数附近设置断点检查next_image_info-args.arg0即要跳转的地址和next_image_info-spsr程序状态保存寄存器包含了目标CPU模式和安全状态位的值是否正确。SPSR的值如0x3c9决定了切换后的状态。问题三系统能进入U-Boot但串口输出乱码或没有输出。可能原因串口时钟或引脚复用配置不一致。排查ATF阶段和U-Boot阶段对同一个串口的初始化配置如波特率、数据位、停止位、时钟源必须完全一致。检查ATF平台代码plat_my_platform.c中的控制台初始化和U-Boot板级配置文件board/.../...c中的串口初始化是否匹配。一个常见的做法是在ATF中初始化串口并打印然后将串口配置“移交”给U-BootU-Boot不应重新初始化导致配置被覆盖。问题四多核启动失败只有主核CPU0启动其他核挂起。可能原因PSCI服务或核间唤醒机制如使用SEI或GIC中断未正确实现或配置。排查首先确认在ATF中ENABLE_PSCI和ENABLE_PMF性能测量框架用于PSCI统计等宏已开启。其次检查平台代码中plat_my_platform_get_core_pos()函数是否正确实现了从MPIDR多处理器亲和性寄存器到核心索引的映射。最后使用调试器在从核的入口函数如secondary_cpu_entry设置断点看它是否被触发。如果没有可能是主核发送的唤醒中断通常通过GIC没有正确送达。实操心得调试ATF最有效的方法是“二分法”和“增加日志”。如果启动死在某个阶段就在该阶段初始化的开始和结束加INFO日志逐步缩小范围。对于时序或并发问题GDB单步调试可能不够需要结合分析日志的时间戳如果开启了ENABLE_PMF可以测量函数耗时。另外一定要善用FVP的--parameter选项它可以模拟很多硬件行为比如关闭某个外设来测试驱动是否健壮。
ARM ATF启动流程全解析:从安全世界到U-Boot的底层调度
发布时间:2026/5/21 9:33:31
1. 项目概述ARM ATF到底是什么如果你在嵌入式开发特别是基于ARMv8-A架构的芯片上折腾过启动流程那么“ATF”这个词你大概率不会陌生。它全称是ARM Trusted Firmware中文常被叫做ARM可信固件。我第一次接触它是在为一个客户调试一块高性能网络处理器时系统启动到某个阶段就卡住了串口日志里反复出现“BL31”的字样当时就一头雾水。后来才知道这就是ATF的核心组件之一。简单来说ATF是ARM官方提供的一套开源固件它定义了从CPU上电复位到将控制权交给上层操作系统比如Linux之间整个安全启动链条的软件实现框架。它解决的是一个在复杂多核系统里“谁先启动怎么启动如何保证安全”的根本问题。在传统的简单嵌入式系统里可能一段Bootloader如U-Boot就直接把内核拉起来了。但在现代ARMv8-A及以后的服务器、移动和物联网芯片里引入了“安全世界”和“非安全世界”的硬件隔离概念这是ARM TrustZone技术启动过程变得异常复杂。ATF就是用来管理这个复杂过程的“总调度员”和“安全基石”。它不是一个你可以直接运行的用户程序而是一套需要编译进芯片ROM或Flash在系统最底层运行的固件代码。理解ATF对于从事底层系统开发、安全启动方案设计甚至是解决某些诡异的启动故障都至关重要。2. ATF的核心架构与组件拆解ATF的代码组织非常清晰它采用了一种分阶段启动的模型每个阶段承担不同的职责像接力赛一样传递控制权。这套模型是理解ATF如何运作的关键。2.1 启动阶段详解从BL1到BL31的接力ATF将启动流程划分为几个明确的阶段通常我们称之为BL1, BL2, BL31, BL32, BL33。这个编号大致代表了它们的执行顺序。BL1 - 可信启动ROM代码这是冷启动后CPU最先执行的代码。在大多数芯片设计中BL1是固化在芯片内部ROM只读存储器里的你无法修改。它的职责非常基础但关键初始化最核心的CPU和必要的片上硬件比如用于加载下一阶段代码的存储控制器建立一个极简的运行环境然后从外部存储设备如eMMC、SPI NOR Flash中加载并验证BL2的镜像。这里的关键词是“验证”。BL1内部会包含一个或多个公钥它使用非对称加密算法通常是RSA或ECDSA来验证BL2镜像的数字签名。只有验证通过才会跳转到BL2执行否则启动失败。这构成了安全启动的第一道防线。BL2 - 可信启动加载器BL2通常是我们开发者可以定制和修改的第一个阶段。它运行在SRAM静态随机存储器中。BL2的任务更重一些它会初始化更丰富的硬件外设如DDR内存控制器将DDR内存配置好然后从存储设备中加载后续所有的固件组件包括BL31、BL32可选和BL33。同样BL2在加载每个组件前也会对其进行验签。此外BL2还负责解析一个重要的配置文件——FIPFirmware Image Package。FIP就像一个压缩包把BL31、BL32、BL33等镜像以及它们的证书打包在一起方便管理和加载。BL31 - 运行时服务固件EL3固件这是ATF的核心也是我们平时打交道最多的部分。BL31运行在ARM特权等级最高的EL3Exception Level 3。你可以把它理解为一个常驻的、特权极高的“安全监控器”或“系统服务管家”。它的核心功能包括世界切换管理“安全世界”Secure World和“非安全世界”Normal World之间的切换。当非安全世界的操作系统如Linux需要调用某个安全服务时比如加解密它会触发一个特殊的指令smcCPU陷入EL3由BL31接管判断请求后可能将执行流转到安全世界的BL32去处理处理完再切回来。电源状态管理接口PSCI为操作系统提供标准的CPU热插拔、挂起/唤醒、系统关机等电源管理功能。Linux内核通过smc指令调用BL31提供的PSCI服务来实现这些功能。安全监控器调用SMC调度作为所有SMC请求的总入口和调度中心。BL31通常常驻在系统内存DDR中不会被覆盖。BL32 - 可信操作系统可选BL32运行在安全世界的EL1或EL0它是一个独立的、小型的可信执行环境TEE操作系统比如OP-TEE。它负责处理具体的、复杂的可信应用TA比如指纹识别、数字版权管理DRM、移动支付等。BL31作为“管家”负责把来自非安全世界的服务请求“派发”给BL32。如果你的产品不需要复杂的TEE功能可以省略BL32。BL33 - 非安全世界软件这就是我们熟悉的“正常世界”的软件通常是U-Boot这类Bootloader或者直接是Linux内核。BL31在完成自身和BL32如果有的初始化后会将CPU的执行等级从EL3降低到EL2或EL1并将控制权交给BL33的入口点。从此系统就进入了我们熟悉的Bootloader或内核启动流程。2.2 关键概念异常等级EL与安全状态要理解ATF必须搞清楚ARMv8的异常等级EL0-EL3和安全状态Secure/Non-secure。EL0用户态运行普通应用程序。EL1内核态运行操作系统内核如Linux内核或TEE内核如OP-TEE内核。EL2虚拟机监控程序态运行Hypervisor用于虚拟化。EL3最高特权级运行安全监控器固件即我们的BL31。只有EL3的代码才能改变CPU的安全状态。安全状态由EL3固件控制。CPU可以处于“安全世界”或“非安全世界”。安全世界可以访问所有内存和外设资源而非安全世界的访问会受到限制。BL31/32运行在安全世界BL33如Linux运行在非安全世界。ATF的BL31驻留在EL3它像一扇门的守卫控制着两个世界之间的通道通过SMC指令和这扇门的开关安全状态切换。3. 环境准备与代码获取在开始动手编译和运行ATF之前我们需要一个合适的开发环境。ATF是高度硬件相关的所以你必须明确你的目标平台。3.1 硬件与软件依赖硬件平台选择对于学习和入门强烈建议使用模拟器。最理想的选择是ARM官方提供的固定虚拟平台FVPFixed Virtual Platform。FVP可以完美模拟一个包含Cortex-A系列CPU、内存、外设的虚拟硬件环境并且完全支持ATF的启动流程和TrustZone。你可以在x86的Linux开发机上直接运行无需任何实体开发板。ARM会为不同CPU模型如Cortex-A57x4, Cortex-A76x4等提供对应的FVP二进制包。如果你有实体开发板那当然更好但请务必确认该板卡的芯片厂商提供了ATF的移植支持。通常芯片厂商如NXP、TI、瑞芯微等会维护自己芯片平台的ATF移植代码你需要在他们的SDK或Git仓库里找到。软件环境搭建Linux开发机推荐使用Ubuntu 20.04 LTS或更新版本。大部分操作在终端完成。交叉编译工具链ATF主要用C和汇编编写。你需要ARM架构的交叉编译器。对于AArch6464位ARM工具链前缀通常是aarch64-none-elf-或aarch64-linux-gnu-。可以从ARM官网或Linaro网站下载。# 例如安装Linaro的交叉编译器 sudo apt-get install gcc-aarch64-linux-gnu构建工具ATF使用make作为构建系统。确保已安装。sudo apt-get install make设备树编译器DTCATF和U-Boot等组件使用设备树Device Tree来描述硬件。需要安装device-tree-compiler。sudo apt-get install device-tree-compilerFVP模拟器从ARM官网下载与你目标CPU模型对应的FVP包并解压。将其路径加入系统PATH环境变量。3.2 获取ATF源代码ATF的源代码托管在GitHub上使用git克隆即可。git clone https://github.com/ARM-software/arm-trusted-firmware.git cd arm-trusted-firmware建议切换到一个稳定的发布分支比如v2.9避免使用最新的开发主干分支以减少不确定性。git checkout v2.9代码目录结构清晰docs/官方文档非常重要入门必读。include/公共头文件。lib/通用库函数如加解密、字符串操作。drivers/通用驱动框架。plat/平台相关代码。这是移植和适配不同芯片的关键目录。你会看到plat/arm/ARM参考设计、plat/nxp/、plat/ti/等子目录。services/运行时服务实现如PSCI、SPM安全分区管理器。bl1/,bl2/,bl31/,bl32/各启动阶段的源代码。注意初次接触时不要被庞大的代码量吓到。我们初期只关注编译和运行流程不会深入每一行代码。重点理解plat/下的平台端口和构建配置。4. 编译与运行以FVP平台为例让我们以ARM的FVP_Base_RevC-2xAEMvA一个模拟双核Cortex-A72的平台为例演示最基础的ATF编译和运行流程。这个流程是理解ATF实际运作的绝佳起点。4.1 基础编译流程ATF的编译通过向make命令传递一系列参数来完成。最基本的编译命令需要指定目标平台PLAT、编译工具链CROSS_COMPILE和构建类型DEBUG或RELEASE。# 在ATF源码根目录下执行 make PLATfvp \ CROSS_COMPILEaarch64-none-elf- \ DEBUG1 \ all fip让我们拆解这个命令PLATfvp指定目标平台为fvp。这告诉构建系统去plat/arm/board/fvp/目录下寻找平台特定的配置和代码。CROSS_COMPILEaarch64-none-elf-指定交叉编译工具链的前缀。make会使用aarch64-none-elf-gcc、aarch64-none-elf-ld等工具。DEBUG1启用调试符号和优化等级-O0方便后续用调试器如GDB跟踪代码。生成产品固件时应使用DEBUG0。all fipall目标是编译出各个BL镜像bl1.bin, bl2.bin, bl31.bin。fip目标则是使用fiptool工具将这些独立的镜像以及可选的BL32、BL33镜像打包成一个fip.bin文件这个文件就是最终要被烧录或加载的固件包。编译成功后你会在build/fvp/debug/目录下找到关键输出文件bl1.bin: BL1镜像注意对于FVP这个BL1是ATF提供的模拟版本真实芯片的BL1在ROM里。bl2.bin: BL2镜像。bl31.bin: BL31镜像。fip.bin: 集成了BL2、BL31等内容的固件包。4.2 构建完整的启动链集成BL33U-BootATF自己并不能直接启动Linux它需要将控制权交给一个BL33。最常用的BL33就是U-Boot。因此我们需要先编译一个U-Boot然后将其作为BL33集成到FIP包中。1. 编译U-Bootgit clone https://github.com/u-boot/u-boot.git cd u-boot # 为FVP平台配置并编译U-Boot。qemu-arm64_defconfig是一个适用于64位ARM虚拟平台的通用配置。 make CROSS_COMPILEaarch64-none-elf- qemu-arm64_defconfig make CROSS_COMPILEaarch64-none-elf- -j$(nproc)编译完成后在U-Boot根目录会生成u-boot.bin文件这就是我们的BL33镜像。2. 重新编译ATF并集成U-Boot回到ATF源码目录我们需要在编译命令中指定BL33的路径。make PLATfvp \ CROSS_COMPILEaarch64-none-elf- \ DEBUG1 \ BL33/path/to/your/u-boot/u-boot.bin \ all fip这次编译fiptool会自动将u-boot.bin作为BL33打包进fip.bin。4.3 使用FVP模拟器运行现在我们有了包含BL2、BL31和BL33U-Boot的fip.bin。FVP模拟器需要一个“闪存镜像”来模拟板载存储。通常我们把bl1.bin和fip.bin拼接起来形成一个完整的闪存镜像。# 在ATF编译输出目录下操作 cd build/fvp/debug/ # 将bl1.bin和fip.bin拼接成flash.bin。bl1.bin需要放在flash的起始地址0x0。 cat bl1.bin fip.bin flash.bin接下来启动FVP模拟器。FVP命令参数复杂但一个最小化的启动命令如下FVP_Base_RevC-2xAEMvA \ --data cluster0.cpu0bl1.bin0x0 \ # 将bl1.bin加载到CPU0的地址0x0模拟ROM --data cluster0.cpu0flash.bin0x08000000 \ # 将flash.bin加载到地址0x08000000模拟Flash -C bp.flashloader0.fnameflash.bin \ # 指定Flash加载器文件关键 -C bp.secureflashloader.fnamebl1.bin \ # 指定安全Flash加载器文件关键 -C bp.ve_sysregs.exit_on_shutdown1 \ # 关机时退出模拟器 -C cache_state_modelled0 \ # 关闭缓存状态建模加速模拟 --console # 启用控制台输出实操心得FVP的参数非常灵活也容易出错。--data参数用于在启动前将文件加载到指定内存地址常用于加载内核或设备树。而-C bp.flashloader0.fname和-C bp.secureflashloader.fname这两个参数是模拟硬件启动流程的关键它们告诉FVP用哪个文件来模拟物理Flash芯片的内容ATF的BL1会从这里去加载BL2和FIP。参数配置错误会导致启动流程断在奇怪的地方。如果一切顺利你会在控制台看到类似以下的启动日志这清晰地展示了ATF的启动接力过程NOTICE: BL1: v2.9(release):v2.9 NOTICE: BL1: Built : 00:00:00, Jan 01 2023 INFO: BL1: RAM 0x4000000 - 0x4008000 INFO: Loading image id5 at address 0x4000000 ... NOTICE: BL2: v2.9(release):v2.9 NOTICE: BL2: Built : 00:00:00, Jan 01 2023 INFO: BL2: Doing platform setup INFO: Loading image id3 at address 0x80000000 ... NOTICE: BL31: v2.9(release):v2.9 NOTICE: BL31: Built : 00:00:00, Jan 01 2023 INFO: BL31: Initializing runtime services INFO: BL31: Preparing for EL3 exit to normal world INFO: Entry point address 0x80000000 INFO: SPSR 0x3c9 ... U-Boot 2024.01 (Jan 01 2023 - 00:00:00 0000) DRAM: 2 GiB ... 看到U-Boot的命令行提示符恭喜你这说明ATF已经成功完成了它的使命安全地初始化系统并将控制权移交给了BL33U-Boot。你可以继续在U-Boot中加载Linux内核和根文件系统完成整个系统的启动。5. 代码导读与关键流程分析仅仅能运行还不够我们得知道ATF的代码是如何组织并实现上述流程的。我们以BL31为例因为它是最核心的运行时服务组件。5.1 BL31的入口与初始化BL31的入口点定义在bl31/aarch64/bl31_entrypoint.S这个汇编文件中。CPU从BL2跳转过来后首先执行的就是这里的bl31_entrypoint函数。它的工作主要是设置异常向量表runtime_exceptions这样当发生中断、SMC调用时CPU知道跳转到哪里处理。从BL2传递过来的参数中获取重要的信息比如BL32和BL33的入口地址、内存布局等。初始化CPU数据cpu_data和系统寄存器。跳转到C语言的主函数bl31_main。bl31_main函数位于bl31/bl31_main.c这是BL31初始化的核心void bl31_main(void) { /* 1. 早期平台初始化 */ bl31_early_platform_setup(); /* 2. 初始化运行时服务框架 */ runtime_svc_init(); /* 3. 平台后期初始化 */ bl31_platform_setup(); /* 4. 初始化BL32如果存在 */ bl31_prepare_next_image_entry(); /* 5. 如果BL32存在则跳转到BL32否则直接准备进入BL33 */ ... }其中runtime_svc_init()函数至关重要。它会遍历一个名为runtime_svc_descs的数组这个数组里注册了所有在EL3提供的运行时服务比如标准服务std_svc包含PSCI功能、spmd_svc用于SPMC模型等。每个服务都定义了自己的处理函数handle当非安全世界发起一个SMC调用时BL31会根据SMC的功能号Function ID路由到对应的服务处理函数。5.2 SMC处理流程剖析SMCSecure Monitor Call是非安全世界与安全世界EL3通信的桥梁。当Linux内核或U-Boot需要执行PSCI操作如关闭CPU时就会发起一个SMC调用。假设一个简单的PSCI调用流程在Linux内核中调用psci_cpu_off()函数。该函数会准备参数并执行smc汇编指令陷入EL3。CPU跳转到BL31设置的异常向量表中的smc_handler在bl31/aarch64/runtime_exceptions.S中。smc_handler会保存当前CPU上下文寄存器状态然后调用C函数smc_handler在bl31/bl31_main.c中。C处理函数解析SMC的功能号。PSCI的功能号有一个特定的范围例如0x84000000-0x8400FFFF。根据功能号找到注册的std_svc服务并调用其对应的处理函数std_svc_smc_handler在services/std_svc/std_svc_setup.c中。std_svc_smc_handler进一步解析子功能号如PSCI_CPU_OFF调用真正的PSCI实现函数psci_cpu_off()在lib/psci/psci_main.c中。PSCI函数执行具体的关核操作操作GIC中断控制器、发送核间中断等。执行完毕后逐级返回最后由异常向量表的代码恢复CPU上下文并通过eret指令返回到非安全世界的调用点。这个过程清晰地展示了BL31作为“调度中心”和“服务提供者”的角色。所有从非安全世界发起的敏感操作都必须经过EL3的审查和处理。5.3 平台移植关键点要让ATF在一个新的芯片平台上运行主要工作集中在plat/目录下。你需要为你的平台创建一个子目录例如plat/my_company/my_platform/。其中必须实现几个关键接口平台初始化函数在plat_my_platform.c中实现plat_my_platform_init()用于初始化该平台特有的硬件如时钟、串口、内存控制器等。内存布局定义在plat_my_platform.h中定义PLAT_ARM_BLx_BASE和PLAT_ARM_BLx_LIMIT等宏明确告诉ATFBL1、BL2、BL31等镜像应该被加载到内存的什么位置。这个布局必须和你的链接脚本linker.lds以及BootROM的加载约定保持一致。控制台串口驱动实现console_my_platform_register()注册一个用于打印调试信息的串口驱动。这是调试的“眼睛”没有它启动过程就是黑盒。电源管理操作实现plat_my_platform_system_reset()和plat_my_platform_system_off()等函数提供系统重启和关机的底层操作。这些会被PSCI服务调用。平台特定的Makefile在平台目录下创建platform.mk定义该平台的编译选项、源码文件列表等。注意事项平台移植最棘手的部分往往是内存布局和缓存一致性Cache Coherency问题。错误的内存布局会导致镜像加载到错误地址直接跑飞。而在多核环境下一个核修改了内存数据另一个核可能因为缓存看不到最新数据导致诡异的问题。ATF提供了flush_dcache_range()等API来处理缓存在操作共享数据结构如PSCI状态时必须小心。6. 调试技巧与常见问题排查调试ATF这种底层固件和调试上层应用完全不同。它没有成熟的OS环境打印日志和利用调试器是主要手段。6.1 日志输出与调试信息ATF使用一个分等级的日志系统ERROR,WARN,INFO,VERBOSE。默认编译DEBUG1会打开INFO及以上级别。你可以在代码中使用INFO(Loading image id%d\n, image_id);来打印信息。确保你的平台控制台驱动已正确实现并注册否则看不到任何输出。在make命令中可以通过LOG_LEVEL参数调整日志级别。例如LOG_LEVEL50会打开VERBOSE级别的详细日志对追踪启动流程细节非常有帮助但输出量巨大。make PLATfvp LOG_LEVEL50 DEBUG1 all fip6.2 使用调试器GDB连接FVP这是最强大的调试手段。FVP支持通过GDB的远程调试协议gdbserver进行连接。首先以允许调试的模式启动FVP并指定一个端口FVP_Base_RevC-2xAEMvA \ ... (其他参数同上) ... --cadi-server port7100 \ # 开启CADI服务器用于性能分析等非必需 -C bp.vis.disable_visualisation1 \ # 关闭图形界面减少开销 -C bp.vis.rate_limit-enable0 \ --no-vis \ -C bp.pl011_uart0.out_fileuart0.log \ # 将串口0输出重定向到文件 -C bp.pl011_uart0.unbuffered_output1 \ --gdbserver port9000 \ # 在9000端口启动gdbserver --parameter cluster0.cpu0.semihosting-enable1 \ --parameter cluster0.cpu0.semihosting-heap_base0x40000000 \ --parameter cluster0.cpu0.semihosting-heap_limit0x1000000然后在另一个终端使用交叉编译工具链中的GDB进行连接aarch64-none-elf-gdb build/fvp/debug/bl31/bl31.elf # 加载带调试符号的BL31镜像 (gdb) target remote localhost:9000 # 连接到FVP (gdb) break bl31_main # 在BL31的main函数处设置断点 (gdb) continue # 继续执行当启动流程执行到bl31_main时FVP会暂停GDB会获得控制权。此时你可以单步执行step/next、查看变量print、查看寄存器info registers、查看内存x命令甚至修改内存值。这对于分析复杂的启动死机问题比如在某个函数里卡住是必不可少的。6.3 常见启动问题与排查思路启动失败时串口日志是唯一的线索。以下是一些典型场景问题一启动卡在“BL1: ...”之后没有任何BL2的日志。可能原因1BL1加载或验证BL2失败。排查检查编译生成的bl2.bin大小是否超出了BL1中定义的加载区域限制BL2_BASE和BL2_LIMIT。检查bl2.bin是否被正确打包进了fip.bin使用fiptool info fip.bin查看。在FVP中确认-C bp.flashloader0.fname参数指向了正确的、包含BL2的flash.bin或fip.bin文件。可能原因2BL2的入口地址设置错误。排查检查plat/fvp/include/platform_def.h中BL2_BASE的定义并确保链接脚本bl2/bl2.ld.S中的入口地址_start与之匹配。问题二BL31打印初始化日志后系统挂起没有进入BL33U-Boot。可能原因1BL33U-Boot镜像加载地址或入口点错误。排查检查传递给make的BL33参数路径是否正确镜像是否有效。在BL31的日志中寻找类似Entry point address 0x...的信息确认这个地址是否是U-Boot镜像的实际入口地址通常是CONFIG_SYS_TEXT_BASE。可以使用aarch64-none-elf-objdump -f u-boot查看U-Boot的入口点。可能原因2CPU模式或安全状态切换错误。排查BL31在跳转到BL33前需要将CPU从EL3切换到EL2或EL1并从安全世界切换到非安全世界。调试时可以在bl31_prepare_next_image_entry函数附近设置断点检查next_image_info-args.arg0即要跳转的地址和next_image_info-spsr程序状态保存寄存器包含了目标CPU模式和安全状态位的值是否正确。SPSR的值如0x3c9决定了切换后的状态。问题三系统能进入U-Boot但串口输出乱码或没有输出。可能原因串口时钟或引脚复用配置不一致。排查ATF阶段和U-Boot阶段对同一个串口的初始化配置如波特率、数据位、停止位、时钟源必须完全一致。检查ATF平台代码plat_my_platform.c中的控制台初始化和U-Boot板级配置文件board/.../...c中的串口初始化是否匹配。一个常见的做法是在ATF中初始化串口并打印然后将串口配置“移交”给U-BootU-Boot不应重新初始化导致配置被覆盖。问题四多核启动失败只有主核CPU0启动其他核挂起。可能原因PSCI服务或核间唤醒机制如使用SEI或GIC中断未正确实现或配置。排查首先确认在ATF中ENABLE_PSCI和ENABLE_PMF性能测量框架用于PSCI统计等宏已开启。其次检查平台代码中plat_my_platform_get_core_pos()函数是否正确实现了从MPIDR多处理器亲和性寄存器到核心索引的映射。最后使用调试器在从核的入口函数如secondary_cpu_entry设置断点看它是否被触发。如果没有可能是主核发送的唤醒中断通常通过GIC没有正确送达。实操心得调试ATF最有效的方法是“二分法”和“增加日志”。如果启动死在某个阶段就在该阶段初始化的开始和结束加INFO日志逐步缩小范围。对于时序或并发问题GDB单步调试可能不够需要结合分析日志的时间戳如果开启了ENABLE_PMF可以测量函数耗时。另外一定要善用FVP的--parameter选项它可以模拟很多硬件行为比如关闭某个外设来测试驱动是否健壮。