ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ARM可信固件ATF深度解析:从架构安全到平台移植实践

2026/9/9 9:33:50 拓冰建站 浏览量
ARM可信固件ATF深度解析:从架构安全到平台移植实践 先抛个结论如果你要做ARMv8/AArch64平台上的可信固件、Secure Monitor、安全启动或者PSCI电源管理Arm Trusted FirmwareATF现在仓库里更多叫TF-A基本是绕不开的一层。如果你在这个圈子里呆过一段时间你会发现固件工程师提到ATF时的语气和Linux内核开发者提到U-Boot很相似——这层东西说难不算难说简单又到处是坑。这篇文章基于我对ATF源码的深度走读和一次实际平台移植经历把它的架构设计、安全工程视角下的审计思路、以及在真实SoC上落地的操作步骤串起来讲。这篇内容适合刚接手平台Bring Up的嵌入式工程师也适合想评估自家固件链安全性、或者准备把ATF带到新板卡上的同事。不会只停留在概念我会把启动流程、服务注册、证书验证、MMU和中断控制器这些关键点都拆开最后给你一份可以直接照着做的移植路径和排错清单。读完后至少遇到BL31起不来、SMC调用异常这类问题你能有一套自己的排查思路而不是在日志里瞎猜。1. 从异常世界模型开始理解ATF先搞清楚它站在哪1.1 ARMv8的异常等级决定了ATF的存在方式很多朋友看ATF源码第一反应是“BL1、BL2、BL31都是啥怎么这么多入口”。但如果你不理解ARMv8的异常模型看这些文件只会越看越晕。ARMv8把系统运行状态分成四个异常等级EL0到EL3。EL0跑普通应用EL1跑操作系统内核EL2给虚拟化场景用EL3才是安全状态的最高权限。ATF最核心的部分就运行在EL3这也是它能管理安全启动和电源状态的根本原因。非安全世界的代码无论如何都无法直接操作EL3资源除非通过SMC指令主动陷入到EL3这种设计把可信固件放到了系统安全边界的最顶端。你可以在脑子里把EL3类比成一个“物业总控室”。普通租户Non-secure OS如Linux在自己房间做什么都行但开关整栋楼的电闸、进设备间、执行紧急预案就必须通过前台SMC请示总控室。ATF就是那个总控室的常驻管理员。所以每次看到ATF的代码在切换MMU、配置中断、设置TZASCTrustZone Address Space Controller时你要意识到它做的不是在“某个操作系统里改配置”而是站在比所有操作系统都高的权限层为整个系统划定安全边界。1.2 从BL1到BL33固件接力赛的每一棒都有讲究ATF把启动过程拆成了BL1、BL2、BL31、BL32、BL33几段。名义上BL32可选一般跑OP-TEE这样的TEE OSBL33通常是U-Boot或EDK2并不是ATF的产物但整条链路的编排和认证都由ATF控制。BL1是最小的ROM代码通常固化在芯片内部ROM里负责最基本的CPU初始化并加载BL2。因为ROM不可改所以它必须是信任链的根。BL2运行在安全世界的EL1负责从存储介质加载BL31、BL32、BL33并做镜像认证和FIP解析。BL2像一个“带质检的搬运工”不是把东西搬完就完事还得验证签名和密钥链。BL31是EL3 Runtime FirmwareATF的最核心常驻程序。系统启动到正常状态后BL31会一直驻留在EL3接收SMC请求提供PSCI、SDEI等运行时服务并负责CPU热插拔、suspend/resume、系统关机这类电源控制。BL32可选通常是TEE OS它运行在安全世界的EL1需要BL31通过SMC帮助进行世界切换。BL33一般就是U-Boot或者UEFI固件它接着加载内核。很多新手会问“那BL31之后ATF还活着吗”。答案当然是活着而且活得比谁都长。BL31初始化完Non-secure世界后把EL1/EL2上下文交给BL33然后自己留在EL3持续服务。你Linux内核里执行CPU hotplug底层就是通过SMC调用到BL31的PSCI服务来实现的。1.3 源码目录与构建产物拿到仓库后先看这些地方理解了启动阶段再看ATF源码目录就轻松了。顶层目录下bl1、bl2、bl31、bl32分别对应各阶段启动代码plat目录放各平台实现lib目录放通用库drivers是各类外设驱动include则是公共头文件。关键文档在docs目录特别是Porting Guide平台移植人员建议先把这份文档读两遍再动手。构建方式一般是基于Makefile以FVP平台为例make PLATfvp DEBUG1 ARCHaarch64 bl31如果平台的BL33用U-Boot先编出U-Boot的BL33镜像再通过fiptool打包成FIP烧到flash里。ATF里的fiptool是一个专门用来打FIP包的工具它会把BL2、BL31、BL32、BL33以及证书一起按固定格式封装。构建完成后产物在build/ /debug|release/目录下常见的有bl31.bin、bl2.bin以及打包好的fip.bin。2. 架构全景里的关键设计信任链、运行时服务与SMC交互2.1 可信启动与认证链不是把镜像加个密那么简单ATF中安全启动Trusted Board Boot的整体思路是建立一条从Root of Trust开始的验证链。ROT密钥的公钥被固话在BL1的ROM代码里。启动时BL1用这个公钥验证BL2的证书和镜像BL2拿到权限后继续验证BL31、BL32、BL33。这个过程里有两个工具很关键cert_create负责生成密钥和证书fiptool负责把证书和镜像打包成FIP。在构建过程中签名密钥通常在开发服务器上预先配置好这些私钥必须保护妥当一旦泄露攻击者就能伪造任何固件通过启动验证。我在实际项目里见过不少把测试证书直接随固件一起发给供应商的案例这种操作等于安全启动的锁形同虚设。你还要理解“签名”和“加密”的区别。ATF安全启动默认关注的是完整性和来源可信也就是确认固件没被篡改确实是授权方发布的。签名的镜像本身可以被你读出来但不代表你能改一个合法镜像。如果有防止泄密的需求需要额外做加密方案ATF在这层也可以配合平台特定的加解密驱动来做。2.2 EL3 Runtime ServicePSCI之外还有哪些服务BL31启动后会进入主循环等着各种SMC请求。每一个SMC调用的Function ID都有编码规则高16位表示服务类型比如ARM标准服务、ARM SIP服务、OEM服务。ATF源码中的services目录就是各种服务的实现其中最常见的是std_svcPSCI电源管理就在这层。PSCIPower State Coordination Interface是ATF最重要的对外接口Linux内核的CPU hotplug、cpuidle、系统重启都靠它。你会发现移植ATF时平台必须实现的接口里有一大堆和电源管理相关的函数比如如何把某个CPU核心上电、如何把一个核心安全地关掉。因为这些操作直接接触硬件寄存器且往往和WFE/SEV等待机制绑定写错了系统会卡死或无故唤醒。除了PSCIATF还支持SDEISoftware Delegated Exception Interface用于把一些虚拟中断事件传递给Non-secure世界处理。SDEI对RASReliability, Availability and Serviceability场景很重要不少服务器平台用它做错误上报。另外还有EHFException Handling Framework、RAS框架等都属于BL31 runtime service的一部分。2.3 SMC交互与世界的切换当Non-secure世界调用SMC指令时处理器陷入EL3。BL31的运行时服务框架根据Function ID找到注册过的服务处理函数再把处理结果返回给调用者。ATF代码里大量出现x0寄存器保存返回值这是SMC调用约定的ABI你改平台代码时不要破坏这个约定否则上层驱动会收到乱值。还有一点特别容易踩坑SMC不仅仅做功能分发它还涉及“世界切换”。当BL32存在时Non-secure世界想访问TEE里的安全服务需要先SMC到EL3BL31再把CPU状态切到安全世界执行TEE。反过来TEE想访问Non-secure内存或硬件资源也需要通过BL31切回。这段切换的上下文保存和MMU切换逻辑是BL31代码中非常容易出错的部分。实际查问题时你可能会在日志里看到类似Unknown SMC Function ID的报错这种多半是上层驱动和BL31固件版本不匹配或者OEM服务ID没有在BL31里注册。不要急着改硬件先用SMC调用者ID和服务ID对照源码里的服务表定位。3. 安全固件工程审计我用五个切入点检视ATF固件链3.1 安全启动链路审计安全固件审计首先看启动链。我在做安全评估时习惯先从这几个问题下手Root of Trust是否真正不可变BL1是否支持回滚到旧版本固件BL2加载的镜像来源是否被严格限制在FIP内部关于回滚ATF虽然本身不强制但平台可以通过OTP一次性可编程存储或其他anti-rollback机制来记录固件版本。如果你在审计一个量产产品发现固件版本号保存在可被普通用户改写的Flash中那整个安全启动都可以被绕过——攻击者只需要降级到旧版本利用历史漏洞即可。在源码层面还要检查镜像加载时BL2是否使用了平台信任的内存源。比如从eMMC读取的镜像在搬运到DRAM之前有没有做DMA来源约束TZASC是否把安全DRAM区域访问控制真的配置生效了。另外一个值得注意的点是证书解析代码的健壮性比如证书长度字段是否可能导致越界读取这类问题在真实攻防中价值很高。3.2 内存隔离与TrustZone配置审计安全固件的核心职责之一是保证安全世界数据不被Non-secure侧读走。ATF的内存映射主要靠MMU descriptor和平台自定义的内存区域助记符来管理。审计时重点看BL31运行所用的安全RAM是否配置了合适的权限是否允许Non-secure可读或可写。更上层的是TZASC。TZASC由EL3配置把系统的DRAM分成交错排列的安全区域和非安全区域。如果你在代码审计里看到TZASC把整块DRAM都配成了安全属性那Non-secure世界的Linux其实啥也用不了或者异常会在内存访问时发生。常见错误是安全区域和非安全区域的地址区间重叠导致Linux访问某个普通内存时被SMC拒之门外这种问题非常隐蔽。我在一次调试中遇到系统偶发死机最终定位是TZASC配置的region粒度设成了256MB但我们只在地址0x40000000处切分了128MB的区域导致剩余非安全区被误判成安全区。这种配置问题在code review里如果只看宏定义不看物理地址分布极难发现。3.3 调试接口和后门风险审计安全固件审计最容易被忽视的是调试接口。ATF支持Secure Debug功能允许通过鉴权认证后使用调试器访问安全世界资源。该功能如果默认开启或者在量产固件里仍保留攻击者就可以通过JTAG/SWD直接读取任意内存。审计时确认三件事量产固件是否关闭了调试鉴权CoreSight DAP是否在BL31启动后锁死以及log输出等级是否保持在合理范围。ATF的LOG_LEVEL如果设成VERBOSE并在生产环境里输出内存地址、寄存器内容会通过串口泄露这对本地物理攻击者来说完全是“送分题”。特别提醒不要在量产固件里留下厂商自定义的“魔法SMC调用”也就是在不公开文档里存在的、可以读写任意内存、跳过认证的调试后门。我在审计中真的见过代码里通过SMC直接调用某个地址来dump内存的平台这比漏洞更可怕因为这是恶意后门。这类问题不光是代码风险也是供应链合规风险。3.4 审计的工具链和Checklist除了人工读码可以参考的工具链包括IDA/Ghidra辅助分析BL31二进制ARM Trace Buffer分析启动流程以及用TF-A自带的fiptool和cert_create工具校验镜像与证书链。如果你手头有FVP或者开发板还可以用JTAG在BL31执行时观测MMU页表、安全寄存器组。整理一份Checklist会更高效是否启用了Trusted Board Boot是否定义了COLD_BOOT_SINGLE_PSOCI等电源策略GIC是否配置了Group0中断给EL3系统是否禁用了Non-secure对TZASC的写权限BL31是否支持CPU_SUSPEND支持的深度是否导致缓存一致性异常日志级别生产环境是否收敛到WARNING以上。这些搞完安全层面的ATF审计基本能覆盖主要风险点。4. 平台移植落地从零开始在新SoC上把BL31拉起来4.1 移植前需要想清楚的事ATF平台移植最需要的不是写代码而是先把“你的平台能提供哪些硬件能力”理清楚。需要确认的最小集合串口控制器地址及时钟、Generic Timer基址、GIC版本GICv2还是GICv3及其distributor/redistributor/re-distributor基址、系统电源控制方式是否由PSCI控制、内存布局。不要一开始就想着把optee和U-Boot全接上。我建议第一次移植只做最小验证BL1-BL2-BL31然后BL31启动后直接跳到U-Boot或者一个最简单的BL33串口能输出日志、能跳转就算成功。TEE后补电源管理的CPU hotplug也可以后补先把冷启动链路跑通再来增加复杂度。再提醒一下如果你用的是带SCPSystem Control Processor的复杂SoCBL31里很多电源管理动作实际是通过SCP通信来完成的。这种情况下MTMessage Handling单元和SCP固件的接口协议需要提前拿到并对照移植别在BL31里自己直接去碰电源寄存器很可能会和SCP打架。4.2 平台目录结构一切从plat目录开始TF-A的移植以plat目录为入口。ATF支持的平台都放在plat/vendor/board这样的路径下比如plat/arm/board/fvp、plat/qemu/qemu。新建平台时最快的方法是把一个相近的平台目录整体复制一份保留Makefile框架替换掉平台名和地址宏定义。重点要改的文件包括platform.mk定义平台名称、源文件列表以及从平台Makefile里引用的宏。plat_def.h平台级配置文件包含主CPU ID、DRAM基址、TZRAM区、GIC地址等。平台实现源文件例如plat_setup.c、 topology.c、power_mgmt.c。必要的外设驱动比如串口console、定时器、GIC驱动。ATF编译时通过PLAT变量指定平台名所以Makefile解析到的目录必须和你创建的目录匹配。直接用一套已经有GICv3、generic timer驱动的新平台目录是最省事的起点。4.3 核心Porting接口逐个过移植最核心的是实现一系列“函数契约”。porting-guide里列出了约几十个平台接口虽然不全实现也能编过但不代表能正常工作。最关键的三个场景如下。平台早期初始化阶段你需要实现bl31_early_platform_setup2在这里把串口初始化、把内存布局信息记下来、设置一些小变量。这个函数调用非常早此时C环境已经可用但很多外设还没初始化所以千万别做复杂操作。接下来是bl31_plat_arch_setup这个函数会配置MMU。ATF的内存管理用了一种平台描述表的方式把内存区段按属性放进去。MMU配错了不是报错这么简单经常是随机的同步异常Data Abort光是看异常地址不一定能看出来。我在初次移植时在这里卡了两天最后发现是某块外设被映射成了Device-nGnRnE以外宽松属性导致访问未对齐寄存器时触发异常。bl31_platform_setup主要是把设备级别的安全配置起来包括初始化GIC、配置TZASC、设置中断路由。在这个阶段你还需要注册各种运行时服务以及通过plat_set_my_stack给特定CPU核准备栈空间。多核的拓扑描述也要实现因为PSCI的CPU_ON必须知道怎么把某个核从WFI里唤醒。不少常规驱动函数串口、timer等应该复用一个“common/drivers”层。如果新平台的启动方式与现有平台差异太大你就得手写新驱动但绝大多数SoC都能在现有代码里找到可参考的对象。4.4 链接脚本与镜像布局BL31不能跑飞平台链接脚本决定BL31镜像里每段代码放进哪个地址。BL31通常运行在安全RAM里这个RAM可能容量很小常见只有256KB-1MB所以代码体积和栈布局都要卡得很紧。链接脚本里有几个区要注意代码段、只读数据段、栈段、页表所在区。栈段通常在BL31镜像的末尾由平台的启动汇编初始化好栈指针。这里有一个经典问题BL31镜像超过分配给它的安全RAM空间运行时不会立刻崩而是在访问到未映射地址时才同步异常。排查时除了看链接器报告还要确认BL2加载BL31的地址和BL31链接地址一致。很多移植启动失败就是BL2的platform_def.h里定义的BL31_BASE和新平台链接脚本的基址差了1MB。ATF本质上不是一个小固件开个DEBUG级别之后日志和函数插桩会让镜像体积膨胀不少。量产构建建议用release模式并仔细检查Image Size是否满足安全RAM容量要求。4.5 首次上板启动编译、烧录、日志三板斧把最小平台骨架搭好后编译命令一般长这样make PLATmyplatform DEBUG1 RESET_TO_BL311 bl31如果你跳过BL1和BL2只用BL31作为唯一EL3镜像需要打开RESET_TO_BL31。这种方式很多新平台开发时常用好处是可以减少变量直接跑BL31调通后再回头加BL1/BL2。烧录时把FIP写到flash对应分区。启动后如果串口有日志说明BL31的console已经工作。BL31的日志一般会显示平台名称、reset原因、进入下一个镜像的地址。看到该类输出后再接U-Boot或自定义的裸机BL33做验证。如果连串口都没有输出先别急着怀疑ATF的时序逻辑。查三件事串口驱动使用的时钟分频是否正确串口引脚mux有没有被其他bootloader占据BL31用到的PLL/晶振是否已经由上一级固件配置好。没有日志而且你不能用调试器时就只能靠硬件手段如逻辑分析仪量串口脚来排查。5. 常见问题速查我在ATF移植与使用中踩过的坑5.1 编译通过但BL31启动即Hang这类问题绝大多数出在内存映射和电源管理两个方向。内存映射方面请检查MMU descriptor里外设地址的属性电源管理方面检查BL31的cold boot入口是否依赖某个未初始化的电源域。建议在BL31_platform_setup里加GPIO翻转或串口打点缩小范围。如果Hang发生在启动前几条指令那大概率不是C问题而是BL31被加载的地址和它的链接地址不一致。用symbol文件对照反汇编看看PC寄存器的值是否落在预期的text段内。5.2 SMC调用返回莫名错误当你从内核或TEE里调用SMC发现返回值是负数或者Unknown SMC时先看你Function ID里的服务类型和调用者ID有没有对齐。ATF的SMC请求头在高位编码了调用者是谁如果调用者ID不是服务认可的handler会直接拒绝。还有一种情况是BL31编译时没有使能对应的服务框架比如平台的platform.mk里没有包含某个OEM服务源文件那即使你的Function ID写得再对也没有handler来响应。检查方法很简单用strings看BL31镜像里有没有对应服务名的log字符串。5.3 GIC配置引发的中断风暴与中断丢失ATF中对GIC的初始化非常容易出错。GICv3时如果redistributor地址配错每个CPU核都无法收到中断。中断丢失和中断风暴是两个方向的症状前者多数是Group0中断没有正确relay给EL3后者多半是某核的优先级掩码设置成了0导致中断无休止地嵌套。排查手段可以在GIC驱动代码里临时加handle的log把每个IRQ号打出来。然后对照硬件中断号分配表确认BL31阶段究竟该收哪些中断。FVP平台上一切正常不代表真SoC也正常因为FVP对GIC时序容忍度很高。5.4 我整理的ATF调试问题速查表现象可能原因优先排查方式BL31无任何串口输出console驱动未初始化/时钟不对/引脚mux确认console的时钟频率和地址宏BL31跳转BL33后很快死机BL33加载地址不对或ATF带过去的context参数异常检查FIP打包时BL33的entry pointSMC调用结果全为-1平台服务未注册或调用者ID错误检查BL31镜像中服务注册表CPU hotplug失败的core启动后异常PSCI的CPU_ON流程没有正确设定启动地址检查psci_validate_entry_point和mailbox实现系统suspend后无法唤醒唤醒中断配给了Non-secure而非EL3检查GIC Group配置和power state request参数Debug构建一切正常release构建却异常优化后某些延迟初始化被调整或stack frame变化用-Og级别或者link map对照5.5 一条实操建议日志永远分层定位一定要打点我对新平台Bring Up的建议是不管功能多简单先把ATF日志等级调高确认每个阶段的输出正常然后在关键接口函数前后打上自己的标记。有些同事习惯烧录后只看最终结果不成功就一脸懵。其实把问题缩小到某个函数附近比调试整个镜像快得多。比如你可以在bl31_main里加一句话打印next image的信息在runtime service注册后打印服务数量在进入BL33前打印最终跳转地址。这些打点可能看起来很笨但在真实开发中能省至少一个下午。最后再补一个我个人很推荐的工程习惯每次修改平台代码前先在git里做一次干净的commit这样排查问题时能随时diff回看。ATF代码整体风格很规范但一些平台相关补丁因为历史原因会和通用代码混在一起维护时一定要保证自己的平台文件和上游主线能平滑分离否则后续每拉一次新版本都要痛苦地合并。这篇内容就到这。ATF这层固件既不像裸机程序那么直白也没有内核那么高的抽象度但正因为它站在安全边界上才值得花精力把每个细节抠明白。如果你准备开始在新平台上移植ATF先按文中的最小验证思路走一遍跑通了再继续加功能。