ATF在IoT SoC上的移植实践与安全固件审计要点 📅 发布时间:2026/9/6 9:35:02 👁 浏览次数: 1. 为什么我会在一颗IoT SoC上死磕ATF1.1 一块开发板引出的“非典型”需求几个月前我拿到一块基于SigmaStar SSD201的双核Cortex-A7开发板。板子本身不贵但需求很明确在它上面把Arm-Trusted-FirmwareATF完整跑起来并最终衔接U-Boot和Linux内核。当时我对ATF的认知还停留在“BL31就是一段冷启动时神秘运行的安全固件”这个层面真正动手之后才发现这套安全固件框架的深度远超预期——从BL1到BL33每一级都有独立的职责、独立的编译产物、独立的加载地址任何一个环节配置错位表现可能都是“完全没有输出”。写这篇内容就是想把这些天啃源码、翻TRM、反复烧写调试的经验沉淀下来。全文围绕三条线展开ATF架构全景、安全固件工程审计思路、平台移植落地实操。适合正在做嵌入式底层、BSP、安全启动或者准备把Linux往非标准Arm板卡上迁移的朋友参考。即便你暂时不碰ATF理解它的分层方式和编译框架对后续看U-Boot、看OP-TEE乃至看Arm内核启动流程都会有帮助。1.2 ATF在Arm生态里的真实位置先回答一个很多人问过我的问题ATF到底是个什么东西如果拿盖楼来类比Boot ROM是地基上预留的钢筋ATF是浇筑的混凝土框架U-Boot是楼里的电梯调度系统Linux内核则是入住后的住户。ATF不负责业务功能它只负责两件事一是在系统启动早期把安全世界和非安全世界的边界划清楚二是在系统运行期间为操作系统提供安全服务调用通道。Armv8-A架构引入异常级别Exception Level之后EL3是最高特权级而ATF的BL31正是常驻在EL3的固件。它不仅负责启动流程的串联还承担了PSCI电源状态协调接口、运行时服务、安全中断处理等一揽子工作。说得俗一点整个系统能不能在正确的特权级上各司其职ATF是那道总闸。很多做应用层开发的同事不理解为什么一颗搭载A7内核的小板子也要引入这么复杂的框架。答案其实很残酷由于U-Boot和Linux内核都运行在非安全世界如果最高特权级的代码不可信那么整个系统在安全面前就形同虚设。IoT设备之所以频繁爆出固件层面的漏洞很大一部分原因就是省掉了EL3固件这一环或者用一段功能残缺的替代品草草收场。2. 从源码根目录拆ATF架构BL1/BL2/BL31/BL32/BL332.1 拿到ATF源码之后先看什么ATF在官方仓库里的代号是TF-ATrusted Firmware-A。把代码clone下来之后第一眼看上去目录并不复杂但每个目录背后都藏着一段启动故事目录/文件作用我通常怎么用bl1/第一级BootLoader冷启动入口一般不做修改但要看它的链接脚本bl2/第二级BootLoader负责加载BL31、BL32、BL33镜像bl31/EL3运行时固件移植重点几乎每个平台都要改bl32/TEE内核如OP-TEE可选没有TEE需求时可以不编bl33/非安全世界镜像通常是U-Boot或UEFIplat/平台相关代码移植的主战场docs/设计文档遇到匪夷所思的问题时回来翻文档Makefile顶层构建脚本通过make PLATxxx触发编译刚开始接触ATF的人最容易犯的错是把注意力全部放在BL31上忽略了BL1和BL2在启动链路中的角色。实际上如果你的平台Boot ROM不支持直接跳转BL31那么BL1和BL2就是整个链路的“开路先锋”。2.2 BL1和BL2从冷启动到加载者的两级跳板BL1是芯片复位后由Boot ROM加载的第一段ATF代码。由于此刻DDR可能还没有完成初始化BL1通常运行在SRAM里代码量被严格控制得很小。它的核心工作之一是初始化一部分内存控制器把BL2从Flash加载进来。BL2则是运行在EL1的“加载器”负责更复杂的镜像加载工作。它会读取Flash里的FIPFirmware Image Package格式镜像把BL31、BL32、BL33分别放到各自约定的内存地址上。这里有个容易被忽略的细节BL2并不只是“复制镜像到内存”那么简单它还要通过Trusted Board BootTBB机制做镜像的签名校验。我在移植时把签名校验临时关掉节省了不少调试时间但量产版本里绝对要重新打开。2.3 BL31的“常住人口”身份PSCI、运行时服务与SPMBL31是整个ATF里最特别的存在。BL1、BL2跑完就结束了BL31却要一直常驻内存作为EL3的运行时固件提供服务。它的两件事务是启动时交接和运行时服务。启动交接是指BL31负责把CPU从EL3切换到非安全世界的EL2/EL1把控制权交给U-Boot或者内核。运行时服务则是内核在运行期间通过SMC指令陷入EL3请求电源管理、系统复位、secure monitor等服务。PSCI就是其中最出名的一组接口。如果你使用的SoC支持SPMSecure Partition ManagerBL31还会充当安全分区管理器为多个安全服务提供隔离的调度环境。这一块在Armv9的CCA机密计算架构里被进一步放大但即便是现在的Armv8-A设备BL31的设计好坏也直接影响整机的稳定性和安全水位。我在阅读BL31源码时比较推荐一个路径先从bl31/bl31_main.c入手看主初始化流程再进bl31/aarch64/runtime_exceptions.S看SMC异常是如何被分发的最后看services/std_svc/psci/下面的PSCI实现逻辑。抓住这条线整个BL31的骨架就清晰了。2.4 BL32/BL33TEE与普通世界的最终落点BL32是可选的TEE镜像比如OP-TEE OS。它运行在安全世界的S-EL1。有些平台用TrustZone做DRM、安全支付、指纹比对这些场景都依赖BL32提供的安全服务。没有TEE需求时ATF会提供一个spd的none实现直接跳过BL32。BL33则是非安全世界的第一个镜像绝大多数嵌入式Linux设备里就是U-Boot。BL31将CPU状态切到非安全世界后把PC指针指向BL33的入口之后的世界主要由U-Boot接管。看这张表会更有全局感镜像层级运行特权级生命周期主要职责BL1EL3Secure启动阶段结束后终止最小初始化加载BL2BL2EL1Secure加载完成后终止加载并校验BL31/BL32/BL33BL31EL3Secure常驻PSCI、运行时服务、安全中断BL32S-EL1Secure常驻可选TEE安全服务BL33EL2/EL1Non-Secure长期运行U-Boot/UEFI引导内核3. 安全固件工程审计信任链是怎样一环扣一环的3.1 从Boot ROM到Linux内核的信任传导安全固件审计的核心是回答一个问题系统凭什么相信一段代码是可信的Arm体系给出的答案是“链式信任”。Boot ROM是芯片出厂时固化的一段代码它是整个信任链的根。Boot ROM验证BL1的签名BL1验证BL2的签名BL2验证BL31/BL32/BL33的签名。每一级都在加载下一级之前用非对称密码算法对镜像进行验签。这样就没有任何中间环节可以被人为注入恶意固件。ATF里实现这条链路的核心是Trusted Board BootTBB。在编译时make会用私钥对每个镜像生成签名在运行时BL1/BL2会使用公钥验证镜像的完整性。公钥的哈希值存在芯片的OTP一次性可编程存储或eFuse里从物理上杜绝了被篡改的可能。3.2 审计时我会重点盯的四个环节做工程审计不是漫无目的地翻代码我有几条固定检查路径第一验证算法的强度。有些平台为了省启动时间会把签名算法从RSA-2048换成RSA-1024甚至有人直接关闭验签。这在AVLAcceptance Vendor List测试里也许能蒙混过关但在真实攻击场景里等于没锁门。第二检查抗回滚机制。固件被人拿到旧版本之后能不能被强制降级如果镜像头部没有版本号比对逻辑或者OTP里没有安全计数器攻击者就可以把设备回滚到有着已知漏洞的旧固件上。我在审计清单里专门有一项检查BL1是否实现了trusted_boot_boot_proof版本检查逻辑以及平台层是否接入了安全计数器硬件。第三梳理DDR地址空间的隔离。BL31常驻内存后它的代码和数据是不允许被非安全世界读写的。Armv8-A通过TrustZone地址空间控制器TZASC或者io静态映射来做隔离。如果平台配置不正确把BL31的地址区间敞开了那整个EL3世界就等于向普通世界开放了后门。第四检查BL31的SMC处理边界。我在读runtime_exceptions.S时特别注意handler是否会校验调用者传入的参数。SMC是从非安全世界进入EL3最直接的通道如果对参数不加以验证非法地址访问、越界读取这类问题就会成为安全隐患。3.3 一次审计中发现的信任边界隐患去年我审计过一个国产IoT平台的ATF移植代码BL31的platform_setup阶段为了快速初始化直接把DDR中一段固定物理地址映射成了Device Memory而且没有通过TZASC限制非安全世界访问。这意味着普通世界的任意进程都可以尝试读写这段地址空间。从代码层面看这个平台在BL31里也没做地址访问权限检查问题被进一步放大。修复方案其实不复杂把这段地址的访问权限明确限制在安全世界并在memory controller层面把region属性改成Secure。这个案例的核心教训是安全审计不能只看有没有签名验签还要看运行时隔离是否真正落地。在输出这篇文章之后我注意到很多做嵌入式开发的朋友对ATF的审计工作并不熟悉常见的误区是把“能用”和“安全”划等号。其实只要坚持上面四个维度的检查大部分安全隐患都能在早期暴露出来。4. 平台移植落地从零开始适配一颗新SoC4.1 移植之前必须摸清的硬件底账ATF平台移植最忌讳“上来就改代码”。我通常先花半天时间把三份资料吃透SoC的TRM技术参考手册重点查启动流程、DDR控制器的基地址、UART控制器的寄存器布局、中断控制器的GIC版本和分发器基地址。官方BSP的U-Boot代码U-Boot里的低层初始化代码往往能直接对应到ATF平台层应该做的事两个仓库可以互相映照。原厂的ATF参考实现如果有IoT厂商发布过同架构的ATF补丁哪怕代码质量一般也比从零开始凭感觉做要可靠得多。以SigmaStar SSD201为例它是一颗Cortex-A7双核SoC支持TrustZone但官方SDK里默认给的是非安全启动方案。要移植ATF就得自己确定UART基地址、GIC基地址、DDR初始化序列这些关键参数。同理FVP或QEMU这些虚拟平台反而提供了更干净的起点因为它们的地址映射是文档化的。4.2 在plat/下搭建平台目录的规范姿势ATF的移植工作90%集中在plat/目录。以官方plat/arm/board/fvp为模板可以快速搭出自己的平台目录plat/myboard/common/ myboard_bl1_setup.c myboard_bl2_mem_params.c myboard_bl31_setup.c myboard_common.c plat/myboard/include/ platform_def.h myboard_layout.hplatform_def.h是所有平台宏定义的核心我在里面主要定义三类内容#define PLAT_PRIMARY_CPU 0x0 #define PLAT_MAX_CPUS 0x2 #define PLAT_PHY_ADDR_SPACE_SIZE (1ULL 32) #define PLAT_VIRT_ADDR_SPACE_SIZE (1ULL 32) #define UART0_BASE 0x1A400000 #define GICD_BASE 0x1C001000 #define GICC_BASE 0x1C002000当我把这套定义摆到SSD201的参考手册面前逐一核对时才发现官方SDK的UART基地址和内核设备树里用的是同一套值这给调试省了不少事。4.3 最小的可运行移植串口输出B31跳转一次成功的ATF移植不必一上来就接PSCI、接OP-TEE。我习惯把目标拆成两个里程碑里程碑ABL1/BL2把BL31加载起来BL31串口有输出。这个里程碑的关键是platform_def.h里的内存布局必须和BL31的链接脚本一致。BL31镜像需要被加载到安全内存的固定地址如果没有TZASC限制通常直接放在DDR的顶部区域。我在plat/myboard/include/myboard_layout.h里这样定义#define BL31_BASE 0x43E00000 #define BL31_LIMIT 0x43F00000 #define BL33_BASE 0x42000000对应BL31的链接脚本bl31.ld.S里要把RAM_EXEC地址范围改成同样数值。这个环节我曾经因为两边数值不一致折腾了整整一个下午。里程碑BBL31把BL33U-Boot加载起来。BL31启动BL33的路径在bl31/bl31_main.c里是通过bl31_prepare_next_image_entry()实现的。它从BL2传递过来的entry_point_info_t结构里拿到BL33的地址和上下文然后执行el3_exit跳转。这里要特别注意在跳转之前BL31会建立好非安全世界的内存翻译表。如果你的平台没有配置MMU相关寄存器跳转过去之后U-Boot会一头雾水地跑飞。很多“ATF没有启动U-Boot”的案例最终定位都是MMU或SCTLR_EL2配置不对。4.4 交叉编译的完整配置项在工程根目录下执行make PLATmyboard \ ARCHaarch64 \ ARM_ARCH_MAJOR7 \ ARM_ARCH_MINOR0 \ CROSS_COMPILEaarch64-linux-gnu- \ BL33../u-boot/u-boot.bin \ DEBUG0 \ LOG_LEVEL40 \ TRUSTED_BOARD_BOOT0 \ GENERATE_COT0 \ all fip几个参数的用意说一下BL33指向U-Boot的bin文件会直接打包进FIP。TRUSTED_BOARD_BOOT0表示暂时关闭签名校验调试时很管用量产前必须打开。LOG_LEVEL40是NOTICE级别。如果调试时想看到更详细的输出改成LOG_LEVEL50ERROR或LOG_LEVEL60DEBUG但生产环境建议调回低级别不然串口日志会拖慢启动速度。ARM_ARCH_MAJOR/MINOR让ATF在编译时针对Cortex-A7选择AArch32还是AArch64支持。注意Cortex-A7虽然支持AArch32但ATF在AArch64模式下也支持它作为BL31运行而且PSCI等服务在两种模式下都有实现具体要看平台的需求。编译产物中build/myboard/release/bl31.bin就是后续要和U-Boot、内核集成到一起的最终镜像。5. 移植与排障实战我踩过的坑和定位方法5.1 Linker script错位BL31镜像超边界的问题第一次把BL31下载进板子串口完全无输出。我先怀疑是编址错误于是回看链接脚本发现我把bl31.ld.S的RAM_EXEC和platform_def.h中的BL31_BASE写成了两个不同的地址。ATF编译时不会报错链接器只认脚本运行时不认平台宏。这种问题的排查手法其实很机械先用反汇编工具看一下BL31的符号地址确认入口是否落在预期的地址区段内aarch64-linux-gnu-objdump -d build/myboard/release/bl31/bl31.elf | head -n 80然后确认bl31_entry_point的地址是否等于BL31_BASE。如果两者不一致优先检查链接脚本里的RAM_EXEC和BL31_BASE是否一致。5.2 串口“静默期”的排查套路ATF启动过程中一级一级的串口输出是有时间差的。如果某一段打印完全没有出现说明这段代码根本没有被执行或者UART的帧格式不匹配。我遇到过一种很隐蔽的情况BL2已经把镜像加载好了BL31也执行了但就是没有串口日志。原因不是代码跑飞而是UART波特率被另一段代码重新配置了。比如BL2里为了加速DDR训练把PLL改了UART的clock source随之漂移printf的输出频率对不上。解决思路是在BL31的early_platform_setup里主动初始化UART而不是依赖前一级留下的寄存器状态。另外推荐一个调试技巧在关键函数入口加上一个GPIO翻转逻辑。有时候串口彻底不可用但GPIO是实打实的物理信号。用示波器或者逻辑分析仪看几根GPIO的电平变化可以快速判断BL31有没有跑到某个点。这个方法在早期Bring-up阶段比任何JTAG都高效。5.3 PSCI和U-Boot的衔接问题ATF跑通之后U-Boot也能启动了但Linux内核在启动CPU1时会出现CPU1: failed to boot。这个问题的元凶通常是PSCI版本不一致。Linux内核的PSCI调用在设备树里会声明psci { compatible arm,psci-1.0; method smc; };如果ATF里的PSCI实现版本是0.2而内核要求1.0调用就会失败。解决方法是检查plat/myboard/common/myboard_bl31_setup.c里PSCI版本号的宏定义改成和内核匹配的版本。另一个容易忽略的地方是CPU hotplug时的挂起地址suspend address。ATF默认使用CPU_ENTER的方式但有些SoC的CPU在WFI之后需要等待安全侧执行电源序列。如果PSCI返回了错误码Linux会有相应的错误打印耐心读内核日志能省很多时间。5.4 DMA和Cache一致性引出的诡异崩溃这是一个比较深的坑。在BL31里做安全服务时如果涉及DMA操作一定要关注Cache的clean/invalidate操作。ATF里有一组宏专门处理这个inv_dcache_range、flush_dcache_range。我踩过一次BL31里给某个安全buffer填充数据但DMA读取时拿到的是旧数据导致加解密结果完全错误。原因是CPU写入cache之后DMA看到的是内存里的陈旧数据。正确的做法是写完buffer之后执行flush_dcache_range((uintptr_t)buf, size);反过来DMA写入完成后CPU读取之前要执行inv_dcache_range否则CPU读到的还是cache里的旧值。这一对操作在移植任何涉及硬件加速器或DMA的ATF功能时都是绕不开的。6. FVP/QEMU虚拟环境下的快速验证思路6.1 为什么我先在虚拟环境中跑通再上真板真板调试有两个痛点一是烧写节奏慢改一次编译、烧写、上电的时间成本高二是缺少灵活的调试手段出了问题只能靠串口日志猜。FVPFixed Virtual Platform和QEMU环境则完全不同它们能提供即时反馈和丰富的trace信息特别适合验证ATF的分层逻辑和启动链路。用QEMU跑ATF有个前提镜像格式要对。我通常先生成FIP再用QEMU的-bios参数加载BL1或者用-kernel直接跳过前级加载。以下是我的常用命令qemu-system-aarch64 \ -machine virt \ -cpu cortex-a57 \ -m 512M \ -bios build/myboard/release/bl1.bin \ -nographic注意-machine virt的地址映射和FVP不同需要根据QEMU的memory map调整平台宏。如果直接套用真板的地址大概率起不来。虚拟环境的作用是验证“逻辑”不是验证“真板接线”。6.2 用启动日志验证各层是否按预期执行在虚拟环境里跑通之后我会特意检查一串关键logNOTICE: BL1: v2.10(debug):v2.10(debug) NOTICE: BL1: Built INFO: BL1: Booting BL2 NOTICE: BL2: v2.10(debug):v2.10(debug) INFO: BL2: Loading image 3 INFO: BL2: Loaded image 3 INFO: BL31: Boot BL33这些日志虽然简单但含义清晰BL1成功加载了BL2BL2成功加载了BL3x系列BL31成功跳转到了BL33。如果日志在某一层断掉就可以直接定位到那一层的代码逻辑。虚拟环境里跑DDR训练之类的东西是没意义的因为QEMU/FVP已经帮你把内存初始化好了。但启动加载链、MMU配置、SMC异常分发这些核心逻辑是真实有效的这也是我在最终烧板之前愿意花一小时跑QEMU的原因。6.3 从虚拟环境迁移到真板时要调整的东西虚拟环境和真板的差异主要集中在地址映射、时钟频率、DDR初始化三个阶段。在QEMU上验证完逻辑以后我会按这个清单逐项对照platform_def.h 里的UART、GIC、DDR基地址是否替换成真板TRM的值。BL31的内存布局是否和SoC的安全地址空间重叠。DDR初始化代码是否在BL2阶段被正确调用真板必须有虚拟环境可跳过。串口时钟源的频率是否和分频器匹配。做对了这些从虚拟环境到真板的切换才会平滑。经验是如果虚拟环境的日志和真板完全一致那么出问题的概率就会降到最低。7. 关于安全固件审计与平台引移植的几个真实心得项目做得越多越觉得安全工作不是堆功能而是严谨排除一切“不可能”。关于ATF的安全固件审计和平台移植我有几点真实的体会审计工作不要只看主路径。我在前面推荐过BL31主初始化流程但真实设备上很多安全隐患藏在外设驱动的初始化代码、中断处理的边界分支、以及异常处理路径里。攻击者通常不会走你预期的路径所以审计清单要把“低概率高危害”的场景也纳入进来。移植一定要先摸清原厂SDK再动手。很多SoC原厂虽然没有公开ATF补丁但他们的U-Boot代码里已经暴露了启动所需的寄存器序列和地址布局。善用这些现成的线索往往比硬啃Arm官方文档快得多。我移植SSD201时就是从SDK的ddr_sysinit.S里摘出了DDR初始化入口然后在ATF的BL2阶段调用它省掉了一口大锅的时间。性能和安全是权衡不是单选。打开签名验签、打开抗回滚、打开TZASC隔离都会增加启动时间和运行时开销。但对IoT设备来说安全水位不够导致的劣后成本往往比那几十毫秒的启动时间昂贵得多。我的建议是开发调试阶段可以适当放宽量产前必须全量收紧并且在审计文档里写明每一步放宽的缘由和恢复策略。ATF只是整个安全链路的起点。BL31跑通之后还要有TEEBL32、U-Boot、内核、文件系统、应用侧多方配合才能真正构建一个可信系统。很多工程师把“ATF启动成功”等同于“安全启动完成”这个误区值得反复强调。整个ATF的移植过程其实是在跟硬件、编译器、链接器、异常级别模型四个层面的机制同时打交道。它没有多高的技术门槛但确实需要耐心把每一层的细节都抠到底。希望这篇文章能帮准备入坑ATF的朋友少走几段弯路也欢迎有不同实践经验的人来交流各自的踩坑记录。