尧图精选

ARM可信固件ATF深度解析:架构、安全审计与平台移植实战

🕒 发布时间:2026/9/8 19:47:14 📁 来源:尧图网络
做嵌入式底层的朋友应该都绕不开一个东西——Arm可信固件ATFArm Trusted Firmware。我第一次真正吃透它是在给一块自研SoC做安全启动适配的时候。当时系统一上电就沿SMC通道报错为了定位内核态到EL3的调用链我把BL31的源码一行行翻过去才意识到这东西远不是一个“放在BL33前面的跳板”那么简单。它是一整套运行在EL3特权级、负责可信启动、运行时服务与功耗管理切换的安全固件框架几乎所有现代ARM服务器、车载芯片、手机SoC的启动链路里都有它的影子。这篇文章是我的深度源码评测加实战笔记主线分三块ATF的整体架构与启动链路、安全固件的工程审计视角、平台移植的真实落地过程。适合正在做固件开发、内核适配、安全评估的工程师也适合实验室里想用FVP或QEMU跑通整个启动流程的同学。读懂这篇文章你就能回答三个问题ATF为什么存在、源码里哪些地方最值得警惕、换一颗新SoC要从哪里动刀。1. 全景ATF在ARM启动链路中的坐标1.1 EL3、安全世界与固件的职责边界ARMv8/AArch64把运行状态分成了四个异常级别EL0到EL3数字越大特权越高。日常跑Linux的EL1是内核EL0是用户态虚拟化则加一层EL2。但真正站在最高点的是EL3它同时掌控着两个世界安全世界Secure World和普通世界Normal World。这两个世界之间并不是简单的一个标志位而是包含独立的内存区域、独立的系统寄存器视图、甚至独立的外设访问权限。普通世界的内核触不到安全世界的内存而ATF就住在EL3负责把控制权在两个世界之间来回搬。所以ATF解决的第一件事是“谁在冷启动时第一个拿到CPU”。答案是BL1它通常固化在片内ROM里。BL1把系统初始化到SRAM可用然后加载BL2BL2再去初始化DDR加载BL31、BL32、BL33。整个过程像接力赛每一棒都在给下一棒铺路。这里的工程意义在于ATF不是操作系统的对手而是操作系统的地基。内核里的psci_system_suspend、CPU hotplug底层都通过SMC指令陷入EL3最终调到ATF的PSCI实现。把ATF理解成一个“运行在最高特权级的微内核”才不会被它那堆汇编入口绕晕。1.2 源码目录结构的评测式俯瞰打开官方仓库ARM-software/arm-trusted-firmware第一眼就值得停下来看。目录划分非常清晰顶层就是启动阶段的代号bl1、bl2、bl31、bl32。BL1是从ROM里跑起来的第一段BL2负责加载可信镜像BL31是EL3的常驻运行时BL32是可选的可信OS比如OP-TEEBL33则是下一棒常见的就是U-Boot。我梳理了一张目录职责表对你快速定位源码很有用目录/模块职责关键文件示例bl1片内ROM启动加载并校验BL2bl1/bl1_main.c、bl1_entrypoint.Sbl2初始化DDR加载BL31/BL32/BL33bl2/bl2_main.c、bl2_entrypoint.Sbl31EL3运行时服务、PSCI、SMC分发bl31/bl31_main.c、runtime_exceptions.Sbl32可信OS或SPM相关bl32/sp_min/services各种EL3服务如std_svc、spdservices/std_svc/lib通用库psci、el3_runtime、context管理lib/psci/drivers控制台、认证、延迟定时器等drivers/arm/plat各平台移植代码这是移植的重点plat/arm/、plat/qemu/等toolsfiptool、cert_create等工具tools/fiptool/对新人来说最容易犯的一个错误是一上来就扎进bl1的汇编里。其实最快建立全局观的方法是先读两遍bl31_main.c和plat/arm/common/里的平台代码把“启动下一个镜像”这件事搞明白再回头啃汇编。1.3 冷启动流程逐级走读整条冷启动链可以在代码层面完整走通我按实际执行顺序给你标注一下。上电后CPU从Reset Vector执行在标准平台里进入bl1_entrypoint.S。BL1会设置异常向量、初始化栈和MMU的基础映射然后调用bl1_main。这里有个细节值得注意BL1运行的物理地址空间往往非常小因为片内SRAM很宝贵所以BL1自己就按BL1_BASE和BL1_LIMIT限定了它的可执行范围。BL1拿到BL2镜像后如果编译时开了TRUSTED_BOARD_BOOT要先过认证再跳转。BL2接管后先初始化平台包括DDR控制器这就是为什么BL2必须在DDR起来之前干活。然后它把BL31、BL32、BL33分别加载到对应的内存基址中间也会做镜像认证。资源就绪后BL2拿到一个bl31_params结构体把BL33的入口地址、DDR布局等信息都塞进去最后跳到BL31入口。BL31的初始化比较重。它首先做early platform setup主要是串口和基础内存映射然后是arch setup建立EL3页表再然后是runtime services的注册。等所有服务注册完BL31会调用bl31_prepare_next_image_entry用一条eret指令切入普通世界的BL33。此时BL31并没有消失它的代码和栈仍然驻留在EL3。之后每一次内核调用PSCI或安全服务都会从普通世界触发SMC陷入EL3回到BL31的异常处理入口。这一套流程的源码路线我建议你亲自跟一遍顺序是bl1/bl1_entrypoint.S → bl1/bl1_main.c → bl2/bl2_main.c → bl31/bl31_entrypoint.S → bl31/bl31_main.c。跟着走完你的全局感就已经超过很多做了两三年移植却只改过平台文件的人。2. 安全固件工程审计信任根、镜像认证与风险面2.1 可信启动链是怎么样建立的ATF里的安全不是“加个密码”而是从物理信任根出发沿着启动链逐级验证。这套机制叫Trusted Board Boot简称TBB。它的基本思想是BL1里固化了RoT公钥也就是信任根的哈希BL1只信任持有对应私钥的固件。BL2的镜像必须由这把私钥签名BL1启动时用内置公钥验签。之后BL2再用它持有的Trusted Key去验证BL31、BL32、BL33。细心的人会发现这其实是一条证书链。cert_create工具负责生成证书RoT Key对应RoT证书Trusted Key证书由RoT私钥签发BL31/BL32证书又由Trusted Key签发。启动时ATF的auth模块会沿着证书链逐级查找、解析、验签。签名校验默认走mbedTLS支持RSA和ECDSA。工程审计时我一般先检查这几个点RoT公钥是不是真的固化在OTP或ROM里BL1的验签路径有没有可以通过修改FIP包绕过的逻辑证书解析的递归深度有没有限制如果平台把RoT公钥放在可写Flash里那整个信任链就形同虚设。2.2 FIP包与镜像认证机制FIPFirmware Image Package是ATF打包镜像的统一容器。无论是BL31、BL32还是BL33、证书最后都由fiptool塞进一个fip.bin。工具本身在tools/fiptool目录命令行很简洁比如把U-Boot加进FIP用fiptool update --nt-fw u-boot.bin fip.bin。它会把每个镜像按照规定的UUID写到FIP包的不同分区。从审计的角度FIP包的好处是格式统一、可通过脚本解析风险点在于它本质是一个信任链的载体而不是安全边界。攻击者能改写FIP包里的镜像只要过不了验签就会被拒但如果平台没开TBBFIP包里的镜像几乎是裸奔的谁拿到就能替换。所以做安全评估时我会先确认编译参数里有没有TRUSTED_BOARD_BOOT1如果没有先不要谈“安全启动”。这一点经常被项目组忽略。2.3 源码审计视角下的常见危险面把ATF当成一份安全固件代码来审几个位置是我重点关注的对象。第一是SMC接口。EL3的SMC入口对所有世界开放输入参数一旦校验不严就可能拿到越权的运行时服务。审计时我会逐个查看services/std_svc和平台自定义的SIP服务检查函数ID的参数范围有没有做边界限制传入的地址会不会落到安全内存之外。第二是缓冲区操作。ATF运行在SRAM和DDR中都有数据拷贝比如镜像加载、上下文保存。任何memcpy、bl31_params解析都要检查长度是否来自镜像头长度字段可信不可信。第三是侧信道与故障注入。ATF毕竟跑在CPU上密码操作在时间和功耗上的泄漏也是风险面。虽然普通项目很少做这么深但在对安全等级有要求的场景里需要评估是否引入常量时间比较、掩码等防护措施。第四是静态扫描。ATF官方在CI里接入了Coverity普通团队也可以自己跑Fortify或Clang Static Analyzer。我实际跑过一次告警里最常见的类型是“uninitialized variable used”“buffer overflow possible”和“dead code”。这些告警不一定全部命中真实漏洞但是能帮你快速找到值得人工复核的代码路径。2.4 给固件工程化的几条安全建议结合我的实际踩坑经验给正在做安全固件的人几条建议。第一非必要不增加EL3服务。每多一个SMC接口就多一个攻击面。如果业务只需要PSCI就把platform定义的SIP服务压缩到最小。第二能关就关调试接口。ATF的串口日志在Debug版里非常详细Release版里也应尽量去掉敏感打印。第三镜像要加密。TBB解决了完整性问题但没解决机密性。如果产品有防抄板需求BL31、BL32镜像需要再做一层加密存储在BL2阶段解密。第四防回滚。新版本固件要加版本号和anti-rollback标识否则攻击者可以用旧版固件替换。3. 深入BL31安全世界运行时服务与PSCI的实现解剖3.1 SMC分发框架一场有序的异常下陷BL31启动完成后就进入了“等待模式”一直在EL3空转。当普通世界或安全世界需要EL3服务时会执行SMC指令触发同步异常CPU陷入EL3异常向量进入smc_handler64.S。这里并不是一股脑地处理而是按函数ID的位域来分发。SMC函数ID的高8位代表服务类型比如PSCI服务、SIP服务、可信OS服务等等。BL31通过runtime_svc框架把这些服务注册成一个个rt_svc_desc结构体每一个都挂了自己的init、handle、reset函数。分发时就是查表找到对应服务描述符再把参数传过去。我自己在移植的时候特别留意了DECLARE_RT_SVC这个宏。它把服务名、服务ID范围、初始化函数、SMC处理函数打包成一个结构体通过链接脚本放进特定的段里。这样新增一个平台SMC服务不需要改框架代码只需要在平台目录里实现一个服务并声明即可。3.2 PSCI的工程实现细节PSCI是ATF里最核心的标准服务。Linux内核的CPU hotplug、suspend/resume、系统重启最终都由PSCI服务接管。在lib/psci目录下psci_setup负责初始化和读取平台电源拓扑psci_cpu_on、psci_cpu_off等函数则对应具体的电源操作。平台在移植PSCI时做的事情就是给ATF提供一套psci_ops。比如cpu_on时平台要告诉ATF怎么唤醒一个从核通常是操作电源控制器或者写一个CPU mailbox。ATF框架负责协议、缓存一致性、上下文切换这些通用逻辑平台只管最底层的寄存器操作。有一个常见误区要特别提醒很多新人以为BL31里配好了PSCI内核就能直接调用。实际上还要看内核侧的psci_checker、设备树psci节点和CPU enable method是否正确。我在调试时不只一次发现ATF没问题反而是内核设备树里cpu的enable-method没写对导致CPU hotplug失败。3.3 上下文保存与切换机制BL31保存在两个世界之间切换时的CPU现场。这一块由lib/el3_runtime和lib/context_mgmt实现核心思路是把普通世界的通用寄存器、系统寄存器、向量表地址、MMU配置等全部压到一块上下文结构体里等切换回来时再整块恢复。上下文结构体看起来很长因为它要覆盖EL1和EL2的可见状态包括一些安全敏感的寄存器比如CNTKCTL_EL1、CNTHP_CTL_EL2等。审计时我会重点看这些寄存器在切换时有没有被正确保存和恢复任何一个遗漏都可能成为CPU状态泄漏的毛孔。从移植角度平台主要需要关心BL31的栈空间和上下文空间在内存中的分配。如果TZRAM布局没排好、栈溢出到其他镜像区域会出现非常难查的“飞针式”崩溃。3.4 框架留给平台扩展的点ATF把平台差异集中在了plat目录。移植一个平台核心是实现一组约定好的函数和宏框架在启动时自动调用。最常用的有bl31_early_platform_setup、bl31_plat_arch_setup、bl31_platform_setup、plat_get_next_bl_params、plat_get_bl_image_load_info。在PSCI这边还要实现plat_setup_psci_ops来填充psci_ops。这种设计很像Linux里的struct ops优点是框架逻辑高度复用换平台只换ops内容。缺点是文档不够集中新人容易找不到该改哪个文件。我在下文第4部分会给一份最小文件清单跟着清单走就不会漏。4. 平台移植从零适配一块新SoC的落地指南4.1 移植前准备资料齐不齐决定坑多深移植ATF最怕的不是代码难写而是文档缺失。要动工一块新SoC至少需要三样资料SoC的Technical Reference ManualTRM里面包含内存映射、UART寄存器、电源控制、复位控制ARMv8/AArch64的架构手册很多情况下你只需要查异常级别、系统寄存器和SMC相关的章节一块参考板或仿真模型比如Arm官方FVP、QEMU的virt平台或者开发板真机。工具链方面ATF新版要求AArch64交叉编译器最常见的是aarch64-none-linux-gnu-gcc或者aarch64-linux-gnu-gcc。老旧的armcc/AC5已经不适合新仓库因为ATF里已经有大量内联汇编和Clang/GCC内建函数依赖。如果你手上只有arm compiler 5.x我建议直接换不然编译会卡在汇编器特性上。环境上我通常建议先跑通一个现成平台再动手新平台。最简单的是make PLATqemu或者PLATfvp能出BL1、BL2、BL31和fip.bin。跑通这个流程会让你对构建系统和几个关键宏建立直觉。4.2 最小移植工程目录和Makefile骨架在plat目录下新建一个平台根目录我习惯用它做演示。以我的经验最小可启动工程至少需要四个文件platform.mk定义平台相关的编译规则和宏platform_def.h定义内存布局和核心宏plat_setup.c实现平台初始化流程plat_helpers.S实现启动阶段的汇编辅助函数。platform.mk里最核心的是PLAT_INCLUDES、BL31_SOURCES、BL2_SOURCES、BL1_SOURCES这几个变量。它们决定了编译哪些源文件。对最小工程你可以只放一个platform.mkPLAT_INCLUDES : -Iplat/myboard/include BL31_SOURCES \ plat/myboard/plat_setup.c \ plat/myboard/plat_psci.c BL2_SOURCES \ plat/myboard/plat_setup.c BL1_SOURCES \ plat/myboard/plat_setup.c $(eval $(call add_define,PLAT_NAMEmyboard))实际编译时还需根据平台特点引入串口驱动、定时器驱动的源文件。平台目录建好后用make PLATmyboard DEBUG1命令构建记录编译串联的依赖关系移植调试就有一个稳定的参考系。4.3 平台定义文件的核心宏剖析platform_def.h是整个移植里最要命的一个头文件。它决定ATF代码编译时能“看到”哪个地址、多大内存、跑到哪里。几个关键宏我列在这里并给一个典型的FVP参考值宏作用典型值以FVP-RE为基础PLATFORM_LINKER_FORMAT链接脚本格式elf64-littleaarch64PLATFORM_LINKER_ARCH链接架构aarch64PLATFORM_STACK_SIZE栈大小0x1000PLATFORM_CORE_COUNTCPU核数0x4PLAT_MAX_PWR_LVL最大电源层级2PLAT_MAX_RET_STATERET状态1PLAT_MAX_OFF_STATEOFF状态2BL31_BASE / BL31_LIMITBL31加载区间0x4000000 / 0x4002000TZRAM_BASE / TZRAM_SIZE可信SRAM区域0x4000000 / 0x40000PLAT_PHY_ADDR_TO_VIRT物理地址转虚拟地址映射函数PLAT_VIRT_ADDR_TO_PHY虚拟地址转物理地址映射函数如果你对内存布局没有把握最稳妥的办法是把BL31放到SRAM里而不是DDR。因为DDR要等BL2之后才初始化BL31如果放在DDR里BL2要先把DDR控制器初始化好。很多人第一次移植失败就是把BL31基址写到了一个还没启动的DDR区域。链接脚本也很关键。ATF的bl31.ld.S会根据平台宏组织代码段、数据段、BSS段如果BL31_LIMIT太小链接阶段会直接报“region overflow”。所以改内存布局时先算BL31镜像多大再回推分配几KB给它。4.4 串口初始化和BL33跳转平台启动后第一件事是让串口能打印。最快的方式是抄一块同类平台的UART驱动通常就是几个寄存器操作使能时钟、设置波特率、初始化FIFO。ATF里console驱动抽象成console_t结构体注册后就能用tf_printf打印。我遇到最多的问题就是时钟频率对不上。比如硬件UART输入时钟是24MHz代码里配成26MHz波特率偏差会被拉大串口打印就全是乱码。调试串口时首选降低波特率到115200排除时钟配置干扰。BL33跳转在BL31里通过next image信息完成。平台需要在bl31_early_platform_setup里填充bl33的入口地址和内存布局比如static bl31_params_t *bl31_params_setup(bl31_params_t *from_bl2) { bl31_params_t *bl31_params (bl31_params_t *)from_bl2; ... bl31_params-bl33_ep_info.pc PLAT_BL33_BASE; bl31_params-bl33_ep_info.spsr SPSR_64_MODE_EL2; ... }注意spsr的选择BL33如果要跑在EL2虚拟化场景spsr设为SPSR_64_MODE_EL2普通Linux通常跑在EL1但一般先跑U-BootU-Boot再降级到EL1。用FVP调试时可以直接把BL33设成一个裸机打印程序跑通后再接U-Boot。4.5 构建、烧写与调试组合构建命令通常是make PLATmyboard DEBUG1 V1DEBUG1会让LOG_LEVEL提高打印更多ATF内部日志。V1是显示完整编译命令排查路径和宏定义很方便。产物在build/myboard/debug/目录有bl1.bin、bl2.bin、bl31.bin、fip.bin。烧写调试分两条路。有关系链完整性的安全启动调试先用JTAGDS-5、OpenOCD、J-Link挂上去设断点看BL1是否进入验签流程不依赖硬件的就用QEMU或FVP跑。QEMU的virt平台对ATF支持很成熟直接make PLATqemu QEMU_USE_GIC_DRIVERQEMU_QEMU_GICV3然后用qemu-system-aarch64 -machine virt -bios fip.bin启动。这个套路用来验证ATF启动链、PSCI行为非常方便。真机调试时建议打开CRASH_REPORTING1如果BL31崩溃它会打印当前异常级别、寄存器上下文这在板上是救命的信息。另外串口日志加时间戳会极大提高定位效率。5. 常见问题与排查技巧实录5.1 编译链接阶段的典型报错编译阶段最经典的报错是“undefined reference to plat_console_init”。原因是平台没有实现串口初始化函数而BL1在reset后第一个想要的就是它。解决办法是找到参考平台比如plat/arm/board/common里的console实现复制到平台目录并加入编译源文件。另一个高频报错是“number of addresses must match the number of relocated addresses”这类链接脚本报错多数是PLAT_PHY_ADDR_TO_VIRT和PLAT_VIRT_ADDR_TO_PHY这两个宏没改对导致MMU区域映射解析失败。它们在platform_def.h里定义FVP上通常是一个简单的加减偏移。还有个容易被忽略的问题BL2源码和BL31源码编译在同一平台但使用了不同的编译选项。比如开启了部分优化却因为BL2跑在DDR还未初始化阶段导致优化出的内存访问踩到未初始化硬件。碰到这类诡异问题先全平台统一用-O1编译试试。5.2 上电后串口沉默的排查顺序串口什么都没打印是移植第一天最常见的噩梦。我的排查顺序是固定的先测硬件链路排除线接错和电平不对然后看代码里有没有执行到console_init如果没有就说明CPU根本没走到BL1的初始化函数再检查UART基址、时钟频率、引脚复用是否正确。如果BL1阶段有输出、BL2阶段突然沉默优先怀疑DDR初始化是否成功。BL2很大概率把问题带到了DDR上例如PHY training失败或者DDR控制器时序没配好。这时可以用JTAG读DDR控制器的状态寄存器确认training完成位。还有一个排查利器是把LOG_LEVEL拉到50看ATF内部日志停在哪一步。很多框架内部操作比如镜像加载、认证、跳转都会在关键节点打日志。看日志的位置基本能锁定是框架问题还是平台问题。5.3 安全启动校验失败的定位开启TBB后最常见的错误是“Authentication failed”。通常有几种原因证书链和镜像不匹配RoT公钥与签名私钥不匹配FIP包里镜像的UUID放错位置或者平台上OTP里的公钥没烧录。我最常用的一条命令是fiptool info fip.bin它会把FIP各个镜像的UUID、大小、offset打印出来再和编译生成时用的清单比对迅速查出是否是打包顺序或UUID错误。证书链验证失败时我会先用cert_create重新生成一整套测试密钥和证书保证使用的密钥和编译时指定的MBEDTLS配置完全一致。如果只是想快速让启动通过可以暂时关闭TBB验证把TRUSTED_BOARD_BOOT1去掉重新编译。但正式产品里一定不能省。调试安全启动强烈建议先用Debug build关掉签名校验确认启动链路本身能通再把认证打开把变量控制在单一维度。5.4 PSCI相关诡异问题的实战经验PSCI的问题最让人头疼因为现象往往在Linux内核侧根源却在ATF。比如CPU hotplug的时候一个核下线后怎么也拉不回来。我排查时先在内核侧看psci命令是否实际发出再看ATF侧是否收到最后看平台电源控制器的寄存器状态。suspend/resume失败有时候和缓存一致性有关。平台在低功耗模式下如果关闭了SCU或L2 cacheBL31在恢复流程里需要先正确重建MMU和缓存状态。这一块我建议仔细读自己平台SoC的电源管理文档ATF的框架处理的是通用协议层底层状态机只有SoC TRM能告诉你。还有一个我踩过的坑BL31在做CPU hotplug时使用memset初始化上下文但由于DMA或cache一致性写入的数据没有被CPU看到。这个问题的排查手段是查看平台的内存属性和cache policy必要时在PSCI的电源操作里加cache clean操作。结尾写到这里ATF的架构全景、安全审计和平台移植三条主线已经讲完了。我个人在实际项目中最深的体会是ATF并不是一个“写完就忘”的固件它卡在芯片和系统软件中间既需要理解硬件细节又需要理解内核的调用语义。调试ATF的过程本质上是在建立一套从芯片寄存器到内核行为之间的完整映射。希望这篇评测式的实战记录能帮你在面对一颗新SoC时少走几步弯路也让你在阅读源码时有一个更清晰的坐标感。最后再分享一个小技巧不管多忙拿到新平台后一定先把FVP或QEMU上的相同配置跑通一次这套“仿真先行”的做法能帮你把硬件问题的干扰降到最低。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →