ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ARM可信固件ATF源码解析:安全启动、BL31与平台移植实战

ARM可信固件ATF源码解析:安全启动、BL31与平台移植实战 做ARM平台底层固件的朋友对Arm-Trusted-FirmwareATF应该都不陌生。我最早接触ATF还是它在GitHub上叫“arm-trusted-firmware”的年代当时为了把一块新做的SoC跑起来啃了整整两周源码和Porting Guide。说句实话ATF的代码并不算难读但它的工程链路长、安全约束多很多人卡在“看懂架构”和“真正落地”之间。这篇东西我想从源码评测、安全固件审计和平台移植三个角度把ATF从全局到细节掰开讲一轮重点是我在实际项目里踩过的坑和验证过的方法。适合正在做BSP、准备从U-Boot/内核向下钻到EL3、或者想搞清安全启动链路的工程师。1. ATF架构全景一段跑在最高特权级的“底层管家”1.1 从BL1到BL33一条完整的信任接力链ARM架构下一个典型的Armv8/Armv9系统启动过程不只是“BootROM加载U-Boot再加载内核”这么简单。ATF把启动过程划分成多个Boot LevelBL每个BL都承担特定的初始化任务同时完成对下一级的认证和跳转。BL1通常烧在芯片BootROM里是系统的第一段代码负责初始化最小硬件环境然后从FIPFirmware Image Package中加载BL2。BL2在DRAM或者SRAM里运行负责更完整的内存和平台初始化并加载BL31、BL32可选、BL33。BL31EL3运行时固件启动完成后驻留内存通过SMC中断处理非安全世界发来的请求比如PSCI调用。BL32可选的TEE如OP-TEE提供安全世界服务。BL33下一级固件最常见的就是U-Boot、EDK2之后才轮到Linux内核。我自己的理解是这条链有点像接力赛中每棒都要检查前一棒的腕带签名从BL1认证BL2、BL2认证BL31/BL32/BL33每一跳都会校验镜像的证书和哈希。ATF里真正干这件事的是Trusted Board BootTBB机制。很多入门教程喜欢用图把启动流程画出来但更重要的是理解“为什么必须分层”——因为BL1体积小、没法做复杂的文件系统解析所以它只做最小的加载BL2有了更大的RAM才做FIP解析和证书链校验。这个分层思路在你看源码时能少绕很多弯路。具体到代码位置BL1入口在bl1/bl1_main.cBL2入口在bl2/bl2_main.cBL31主逻辑在bl31/bl31_main.c。建议第一次通读时就从这三个文件加bl_common.c开始先跟着启动顺序走一遍再回头研究细节。1.2 EL3、安全世界与非安全世界为什么ATF“权限最高”Armv8-A架构定义了四个异常等级EL0、EL1、EL2、EL3数字越大特权越高。Linux内核跑在EL1虚拟化场景下Hypervisor在EL2而ATF运行在最高的EL3。同时系统还有“安全世界”和“非安全世界”之分ATF的一部分代码运行在安全状态负责管理TrustZone隔离。这两个维度组合在一起就形成了我常说的“CPU世界的最高裁判”Linux内核想开关CPU、想进入低功耗状态、想访问被TrustZone保护的外设都不能直接操作硬件寄存器必须通过SMC指令陷入EL3由ATF来执行真正的高特权操作。ATF通过PSCI协议Power State Coordination Interface向非安全世界提供CPU_ON、CPU_OFF、CPU_SUSPEND、SYSTEM_OFF、SYSTEM_RESET等标准接口。所以ATF不是一个“可有可无的引导小程序”它是整个系统安全模型和电源管理的中枢。这也是为什么现在几乎所有手机、服务器、嵌入式ARM SoC都离不开这段固件。你在看ATF源码时会发现大量代码都是在处理这种“世界切换”和“异常级别切换”这比普通应用级固件要难写得多也容易藏问题。1.3 源码目录结构读ATF不要一头扎进去ATF源码已经改名成Trusted Firmware-ATF-A但很多人还是习惯叫ATF。它的代码库经过多年发展目录也多了很多但核心骨架比较稳定。目录作用我的阅读优先级bl1/、bl2/、bl31/、bl32/各级固件入口与主逻辑高common/平台无关的启动、运行时公共服务高plat/平台相关代码如plat/arm/fvp高lib/内存、延迟、锁、编译器运行时支持中drivers/串口、GIC、TZC400、DDR等驱动中services/SMC服务如PSCI、SPM、RAS高include/头文件以及AArch64寄存器定义中个人经验如果第一次接触先看plat/arm/board/fvp即Fixed Virtual Platform参考平台因为这个平台代码相对完整而且大部分人即使手头没有FVP也能靠QEMU跑起来。先从平台代码知道“一个平台需要实现哪些函数”再去读BL1/BL2/BL31的框架心里才会有具体画面。反过来先啃BL31的代码很容易被各种ops结构体搞晕。2. 源码级工程审计安全固件的关键路径到底查什么2.1 安全启动链路与信任根别让签名校验变成摆设做ATF安全固件审计第一件事就是看安全启动链路。TBB机制的信任根Root of Trust通常是一个可信的公钥ROTPKRoot Of Trust Public Key它被烧在OTP/efuse里或者硬编码在BL1的只读区域。在代码层面你会遇到下面这些关键接口plat_get_rotpk_info()返回平台信任根公钥的存放位置和内容。auth_mod、auth_common、crypto_mod证书解析和签名验证逻辑。证书链调用关系BL1验证BL2的证书BL2验证BL31/BL32/BL33的证书。审计时我最关心的几个点也是实际项目出过事的点第一信任根是否真的被“硬件保护”。如果ROTPK只是放在一个可写Flash里攻击者能改掉公钥那后面验什么都没意义。第二签名校验的失败路径是否被正确处理。有的代码在校验失败时直接返回错误却没有终止启动流程结果就是攻击者拿到一个未签名的镜像照样能跑。第三证书链是否覆盖了所有镜像。有些平台为了图省事跳过了对BL33的验证等于信任链在最后一步断了。我通常的做法是把这几个函数从BL1到BL31逐个打日志打印证书类型、签名状态和镜像哈希。如果某个镜像的校验结果是“跳过”或“未配置”就要好好查一下了。2.2 平台抽象层与硬件隔离PSCI和GIC是要点ATF把硬件差异全部封装在平台相关的函数里核心是plat_psci_ops结构体。比如cpu_on、cpu_off、cpu_suspend、cpu_pwr_domain_on等函数。plat_setup、plat_get_next_bl_params、plat_put_next_bl_params等平台操作。GIC初始化、TZC配置TrustZone Address空间控制器。安全审计时我会重点看这几个点cpu_suspend/cpu_off是否有状态机的完整性校验会不会因为参数越界导致进入异常电源状态。GIC设置中安全中断和非安全中断是否正确路由。如果安全中断被路由到非安全世界等于给攻击者开了一扇门。TZCTrustZone Controller设置了哪些内存区域为安全区哪些为非安全区。常见的低级错误是安全世界的栈内存被配成了可被非安全读写的属性直接泄露敏感数据。这些代码往往不是ATF的公共部分而是每个芯片原厂维护的私有部分所以这是芯片BSP安全Review的重点。很多安全问题不是出在ATF官方代码而是出在OEM/ODM的平台适配代码上。2.3 异常处理与SMC调用把“安全边界”当成第一道堤坝SMC调用是安全世界与非安全世界的“大门”。ATF内部通过handle_smc和rt_svc_desc结构体来分发请求。每一个SMC Function ID都有自己的OENOwning Entity Number比如ARM_SIP_SVC_OEN表示由芯片厂商定义的服务ARM_APCI_SVC_OEN表示PSCI标准服务。从审计视角看SMC入口是必须反复检查的地方参数长度和指针是否做合法性校验。比如非安全世界传进来的地址ATF拿到后直接解引用这就有严重的防绕过风险。返回值是否会泄漏敏感地址或安全内存布局。有些平台为了方便调试在SMC接口里返回了内存地址生产环境如果不去掉等于给别人提供了内核漏洞提权后的进一步信息。服务注册是否混乱。DECLARE_RT_SVC注册的服务越多被攻击面就越大。对于不用的SIP服务建议直接裁剪掉。我在一个项目里曾遇到一个内部工具版本为了支持某个调试功能把一块安全内存的物理地址通过SMC直接返回给U-Boot。结果这个功能到了生产版本没删干净被安全团队扫描出来才意识到这种“临时后门”有多危险。2.4 安全审计的工作流建议从静态扫描到芯片安全评估ATF这种固件的代码审计不能只靠肉眼扫一遍。现在不少芯片公司的安全评估都会走下述流程用clang-tidy或cppcheck做静态扫描重点关注memcpy、memmove、越界访问。用CodeQL或自定义规则查“危险调用模式”比如在EL3直接使用非安全内存地址。构建多个恶意镜像验证签名校验是否真的拦截非法镜像。用JTAG/SWD接上开发板尝试在启动过程中注入异常看ATF会不会“安静地”挂掉还是输出错误信息。开发阶段一个很好用的配置是DEBUG1和LOG_LEVEL50可以打印非常详细的日志。但量产固件必须把日志等级调低否则光日志信息就可能泄露内部内存布局。3. 平台移植落地指南从零把一个新SoC跑起来3.1 移植前准备先搞清楚三张图我不太建议一上来就抄某个平台的代码。移植ATF必须先搞清楚三张图内存映射图、中断路由图、电源状态图。内存映射图决定了BL31该放在哪里BL32的base地址是多少DDR的哪些区域是安全的哪些是安全的。中断路由图决定了GIC的Interrupt Group配置哪个中断是Secure Group 1哪个是Non-Secure Group 0。电源状态图决定了PSCI要支持哪些状态比如CPU idle有没有LPI、系统有没有S3/S4等级别。这些信息通常散落在SoC的TRMTechnical Reference Manual和安全启动文档里。如果手上只有一张几十页的芯片简介做平台移植是非常痛苦的。我见过不少板卡方案商拿不到完整TRM只能靠逆向或参考同系列芯片代码去猜寄存器最终效率极低。一个务实的办法是先找一颗和你的SoC同系列、同架构的芯片作为“参考平台”。比如用FVP或者任何相近的Cortex-A53/A72平台先把ATF跑起来再逐步替换平台代码。3.2 在plat/下创建自己的平台目录最小文件集ATF每个平台的代码都在plat/vendor/platform下。以创建一个叫mydemo的平台为例最精简也需要以下几个文件platform.mk声明平台源文件、编译参数、Image base地址。platform_def.h定义平台相关的宏如PLAT_PRIMARY_CPU、PLAT_MAX_PWR_LVL、PLAT_ARM_TRUSTED_SRAM_BASE。plat_setup.c、plat_pm.c、plat_topology.c实现平台初始化、电源管理、拓扑描述。plat_sip_svc.c如果需要自定义SMC服务可以放这里。我在刚开始移植时最喜欢把plat/arm/board/fvp的对应文件整个复制过来然后逐步裁剪。虽然ATF官方不推荐“复制粘贴”式移植但对一个还没有完全理解的平台来说这个方法最快。关键是要把platform_def.h里的地址、UART基地址、GIC基地址全部换成自己SoC的真实值。一个最基础的目标是能通过UART看到ATF的启动日志比如NOTICE: BL1: v2.8(release):这行字。只要能打出这行字说明BL1到BL2的调用已经通了。3.3 核心配置与编译命令从Makefile到fip生成ATF的编译并不是直接make就完事它依赖平台Makefile定义的各种变量。以我常用的命令为例make PLATmydemo DEBUG1 \ ARM_ARCH_MAJOR8 \ TRUSTED_BOARD_BOOT1 \ GENERATE_COT1 \ ARCHaarch64 \ CROSS_COMPILEaarch64-linux-gnu- \ all fip这里的“all”会生成BL1、BL2、BL31的二进制文件“fip”会使用fiptool把它们打包成一个fip.bin。如果开了TBB还会调用cert_create工具生成证书链。在国产ARM平台上比如飞腾、鲲鹏、瑞芯微等经常需要在x86主机上做ARM交叉编译这时CROSS_COMPILE指向工具链前缀即可。像用arm交叉编译这个词说的就是这个场景。需要注意的一点是ATF的编译对工具链版本比较敏感我用GCC 9.x、10.x都编过老版本ATF但新版ATF已经默认要求GCC 11以上或Clang 12以上了。如果用的是传统Arm Compiler 5/6就是常说的armcc/armclang编译时务必确认AC6是否满足-Werror的代码规范很多老外挂代码在AC6下会直接报错。3.4 移植中最容易踩的坑内存、MMU与链接地址我给新接触ATF的工程师做评审时几个高频问题几乎都出在地址上。第一个坑是BL31_BASE地址和链接脚本不一致。platform_def.h里的BL31_BASE必须和链接脚本bl31.ld.S里的基地址完全一致否则BL2跳转到BL31后第一条指令就可能跑飞或者访问到未初始化的内存。排查方法很简单在BL31入口打印pc和linker地址差一个常数都很容易发现。第二个坑是MMU配置。ATF在各BL启动后会开启MMU内存区域的属性设备内存还是普通内存、可缓存还是不可缓存、是否可执行都在平台代码的plat_get_mmap_entries或plat_arm_get_mmap_entries里配置。如果DDR区域被配置成不可缓存但CPU里跑的是普通内存类型性能会骤降更严重的是安全世界所需的buffer如果被配成非安全可读写安全边界就直接破了。第三个坑是cache line对齐。尤其是SMC传参时如果源地址或目的地址没有按CACHE_WRITEBACK_GRANULE对齐会导致缓存一致性问题数据看起来对了实际过一会儿就被覆盖。我在调试一个TEE与REE通信bug时折腾了三天最后发现就是一个没对齐的缓冲区。移植时我习惯在每个关键阶段加一个自定义宏做printf比如#define MY_PLAT_DEBUG在plat_setup、bl31_platform_setup、PSCI的pwr_domain_on里打地址和状态。这样能快速定位是“没进函数”还是“函数内部跳飞了”。4. 安全固件工程化从能跑到敢量产还差很多4.1 量产固件安全基线这些开关不能省实验室里把ATF跑通和真正量产物隔着整条安全基线。简单列几个我认为不能省略的配置项TRUSTED_BOARD_BOOT1开启TBB认证否则固件镜像可以被任意替换。GENERATE_COT1生成证书链配套使用cert_create工具。MEASURED_BOOT较新版本支持记录各个镜像的哈希到安全存储用于远程证明。信任根公钥存放在OTP/efuse不要只放在Flash的普通分区里。EL3日志等级调低量产镜像不要开LOG_LEVEL超过30最好为INFO以下。BL32启用并配置OP-TEE没有可信执行环境的ATF安全能力会大打折扣。这些开关不是“建议打开”而是“不打开就别谈安全”。我见过部分物联网设备为了节省存储和启动时间把TBB直接关了整个安全启动形同虚设。即便你不做高安全认证只要有外部输入攻击面签名校验都是底线。不过也需要说明打开TBB之后升级固件会复杂很多。你必须维护一套证书链每次烧录还要把新证书/密钥同步到生产环境。很多开发团队就是嫌麻烦而不开但这部分工作迟早要做。4.2 BL31、U-Boot与内核的配合链路中间卡壳平台移植过程中最常见的一类问题不在ATF自身而在ATF与下一级固件的衔接。比如U-Boot在加载ATF时通常会在u-boot.dts或u-boot.env里指定BL31的加载地址这个地址必须和platform_def.h里的一致。如果你改了ATF的布局U-Boot没跟着改就会出现“BL1启动正常跳BL31后死机”的现象。热词里大家搜“arm bl31 uboot”搜得最多就是这个原因。再比如PSCI的Version查询。U-Boot启动时会调用SMC来获取PSCI版本如果你的ATF没实现PSCI_VERSION或返回版本偏旧U-Boot可能会走老路径进而影响后续的CPU启动。调试这种问题最好先在U-Boot命令行下执行psci version如果有这个命令或者自己写个裸机小程序直接发SMC请求。内核侧也类似。psci-cpuidle驱动的DTPM节点、设备树里的psci{ ... }节点都需要和ATF实现的PSCI功能匹配。有的内核版本要求PSCI至少0.2而早期ATF可能默认只支持0.1这时必须翻ATF配置。4.3 常用调试工具与工具链漫谈ATF调试我自己长期用的几样工具OpenOCD GDB直接在SoC上设断点查看EL3寄存器。ARM DS/Development Studio商业工具调试体验更好但贵。QEMU支持machine virt可以直接跑FVP版的ATF适合新人学习和验证改动。串口日志定位在common/和plat/里加printf注意ATF的printf在DEBUG模式下才支持正式版默认没有。工具链方面aarch64-linux-gnu-gcc是最常见的。很多ARM SoC原厂提供自定义工具链比如针对Cortex-A系列的arm-none-linux-gnueabihf-gcc32位或armclang。编译ATF必须明确是AArch64用错工具链通常会在汇编阶段报很多怪错。热词里提到的“arm compiler 5.06”主要用于Cortex-M等裸机开发编译ATF基本不用AC5别浪费时间去下载老版本了。真正用得上的是Arm Compiler for EmbeddedAC6偶尔会有项目要求用AC6替换GCC做代码安全编译。如果遇到编译报错比如“implicit declaration of function”或者“undefined reference to plat_xxx”大概率是平台代码里少加了文件到platform.mk。ATF的构建系统不像CMake那样自动收集源文件必须手动维护BL31_SOURCES、BL2_SOURCES等变量。4.4 一个典型问题的排查实录ATF启动后死在BL31我拿一个真实经历来演示排查思路。不久前给一块新板子移植ATF现象是U-Boot的booti执行后系统没有任何反应串口停在U-Boot的最后一行日志。第一步通过OpenOCD连接JTAG在BL31入口设断点发现代码根本就没进BL31。说明问题出在U-Boot向BL31跳转的地址或参数。第二步检查U-Boot环境变量里的loadaddr和bl31_addr发现U-Boot把BL31加载到了0x44000000而ATF的BL31_BASE是0x44400000。两边差了0x400000当然跳飞了。第三步修改设备树指定正确的BL31地址重新编译U-Boot再次启动BL31日志出现了。这类问题在移植时特别容易忽略因为你往往只关心ATF编译是否成功却忽略了Bootloader侧如何引导它。建议在移植文档里同时标出U-Boot和ATF两侧的地址约定避免团队里两个人各改一边。5. 给准备入坑ATF的工程师几句掏心窝的话5.1 源码评测哪些值得逐行读哪些只看结论就好读ATF源码我比较推荐“重点深挖、其余略读”的策略。必读的几块有BL31主流程从bl31_main到bl31_lib_init理解SMC分发怎么起来。PSCI实现尤其是psci_setup和psci_cpu_on这是电源管理的核心。平台ops体系plat_psci_ops、plat_pm_ops看明白平台怎么把硬件注册进来。TBB认证模块哪怕你不做安全认证也要理解证书链能防什么。至于驱动部分比如每个平台的DDR初始化ATF也是调用厂商的库函数不需要深入研究实现。这类代码频繁更新还带公司私有算法看了意义不大。我自己的经验是与其硬读全部源码不如带着问题去读比如“U-Boot如何通过SMC调用ATF关机”直接搜索SYSTEM_OFF和psci_system_off沿着调用链一路看下去记忆会更牢固。5.2 我的个人避坑清单这些年陆陆续续给团队整理过一份ATF避坑清单这里拿出来分享几条不要完全复制FVP平台的代码至少要理解每一行的注释和宏含义否则后期改驱动会很痛苦。编译务必加DEBUG1和LOG_LEVEL50否则出了问题日志信息根本不够用。平台宏命名不要和公共宏冲突。比如PLAT_ARM_*这类前缀属于官方平台自己定义最好用MY_*。任何时候改动platform_def.h里的地址都要同步检查链接脚本ld.S和所有外设基地址。使用版本控制时把conf.mk或build/目录加入.gitignore因为ATF构建产物里包含绝对路径不同机器编译容易产生差异。不要忽略EL3中的VERBOSE_PRINT或CRASH_REPORTING宏这些对问题分析帮助巨大。5.3 后续还能怎么扩展ATF不是一个静态项目。现在ARM已经推了FF-AFirmware Framework for A用于设计灵活的分区管理ATF里面很多代码其实就是FF-A的组件化RMERealm Management Extension和Arm CCA也在逐步演进。如果你正在做Armv9平台那ATF的新特性会远多于老的Armv8版本。另一个方向是OP-TEE与ATF的配合。很多团队在移植ATF时只做BL31但量产系统基本都离不开BL32的TEE。接入OP-TEE的成本比想象中低因为ATF已经封装好了bl32的加载和SMC接口只要把OP-TEE的镜像编进去再在平台代码里配置BL32_BASE即可。但真正调试起TEE和REE的会话时各种缓存一致性和共享内存问题又会接踵而来。我个人在实际操作中最大的体会是ATF看着是ARM官方的东西但它的平台化程度比Linux内核更重。你在网上能找到的源码解读再多都不如拿到一块真实板子跑一遍BL1到BL31然后亲手改一个Power Domain体会来得深刻。移植过程中那些反复对地址、调缓存、看GIC的日子才是真正把ATF学明白的过程。最后分享一个实用小技巧在BL31启动后你可以把plat_get_rotpk_info的返回值打印出来确认信任根是否正确烧录。这既验证了安全链路也顺便检查了OTP驱动一举两得。
返回列表