ARTICLE DETAIL

资讯详情

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

Arm Trusted Firmware(ATF)移植实战:从BL31到PSCI的源码级剖析与调试指南

Arm Trusted Firmware(ATF)移植实战:从BL31到PSCI的源码级剖析与调试指南 不知道你有没有这样的经历拿到一块新板子芯片手册翻得滚瓜烂熟U-Boot也能跑起来结果一接ATF就一脸懵。要么编译过了上电没输出要么BL31跳转后系统直接卡死要么PSCI调用返回乱码。我前前后后给三四个平台做过Arm Trusted Firmware的移植和审计踩过的坑比吃过的盐还多。这篇文章不是来讲PPT概念的我会直接从源码层面把ATF拆开把BL1/BL2/BL31各自干什么、安全启动怎么串起来、平台移植时到底要改哪些文件以及我实际调试中遇到过的问题一并讲清楚。适合正在做ATF移植的BSP工程师、做固件安全审计的安全工程师以及想搞懂Trusted Firmware运行机制的底层爱好者参考。1. ATF到底在Arm系统里解决了什么问题一段最容易被误解的启动链很多人一上来就看BL1、BL2、BL31的代码但没搞明白ATF出现的根本原因导致越看越乱。我建议你先搞懂它解决的核心矛盾一个典型的Armv8/Armv9系统里有好几个世界——正常世界Normal World、安全世界Secure World还有比它们更高权限的EL3异常等级。谁来管理这个世界的切换谁来保证安全世界不被普通世界非法调用谁来提供电源管理、系统重启这些底层服务这些就是ATF的活。1.1 从上电复位到操作系统启动的完整接力过程Armv8架构定义了四个异常等级EL0最低EL3最高。ATF的代码集中在EL3和EL2/EL1这几个层级。你可以把整个启动过程想象成一场接力赛BL1Boot ROM阶段SoC上电后CPU从ROM里取的第一段代码由芯片厂商固化ATF源码也提供BL1实现。它做的事情很少初始化最小系统串口、内存控制器的一部分、把BL2镜像从Boot Device加载到SRAM或内存里、验证BL2签名如果开启安全启动然后跳转。BL2Trusted Boot Firmware运行在EL1安全世界负责更完整的平台初始化然后逐个加载BL31、BL32、BL33并对它们做校验。校验通过后跳转到BL31。BL31EL3 Runtime Firmware这是ATF最核心的部分。跳进来之后它不会再退出而是常驻在EL3作为整个系统的安全监控器Secure Monitor。所有非安全世界想调用安全世界功能的请求都会通过SMCSecure Monitor Call指令陷入EL3由BL31分发。BL32可选Trusted OS也就是OP-TEE这类安全世界操作系统跑在安全世界的EL1提供安全服务。BL33Normal World Bootloader通常是U-Boot或UEFI跑在正常世界的EL2最终引导Linux。我刚开始看这套流程时总把BL理解成第几阶段引导后来发现不对——BL更像是启动责任的接力棒每一棒只做自己该做的事然后干净利落地把控制权交出去。这个设计的原因很简单BL1跑在ROM里没法打补丁所以代码必须尽量小且稳BL2跑在SRAM/内存里可以承载较多逻辑但仍然是一次性的真正长期活着的只有BL31。1.2 所有移植者必须先建立的三级镜像依赖心智模型做ATF移植脑子里必须时刻有一张依赖表BL1依赖SoC ROM的启动约束BL2依赖BL1加载BL31依赖BL2完成基础内存初始化BL32和BL33依赖BL31创建的安全上下文。这里有一个最常见的认知误区以为BL31只是个引导程序跳转到U-Boot就功成身退。实际上BL31是所有异常级别里唯一永久存活的固件它要响应运行时的各种请求例如系统休眠唤醒、CPU热插拔、安全存储调用等。移植时你写的很多平台代码不是给启动用的而是给运行时用的——比如plat_get_syscnt_freq2这个函数在系统启动后每个tick都可能被调用plat_psci_ops里的cpu_on/cpu_off是操作系统运行过程中动态调用的。意识到这一层你就明白为什么ATF移植不能只把启动点亮就完事。这个模型还会影响你如何裁剪代码如果平台不需要TEE可以编译时不带BL32如果平台只用U-Boot而不用UEFIBL33就是U-Boot如果有ARMV8的Secure EL2或RME需求可能还要引入SPMD/SPMC这些组件那镜像依赖关系又会多一层。2. 源码目录与启动流程逐层拆解从入口到runtime服务的一次源码级巡检这一节我带你走一遍ATF源码重点不是把每个函数贴出来而是告诉你看源码时应该从哪几个关键文件切入。我以主流版本为例虽然不同版本路径略有差异但整体骨架多年没变。2.1 BL1到BL33入口代码的职责边界与调用关系打开ATF源码仓库https://github.com/ARM-software/arm-trusted-firmware顶层目录有bl1/、bl2/、bl31/、plat/、drivers/、include/、lib/、services/、tools/。不要一上来就把所有代码读一遍按启动顺序读入口就行。BL1入口关键路径bl1/bl1_main.c里的bl1_main()是整个BL1的C语言入口。它初始化运行时环境之后调用bl1_load_bl2()加载BL2镜像最后跳到BL2的入口。BL1的一个重要特点是它存放在Boot ROM里因此代码要足够精简并且启动完成后它所在的内存区域可以被后续镜像回收利用。你在做平台移植时看到BL1_RO_BASE、BL1_RW_BASE这些宏就是定义BL1的RO和RW区域在内存空间中的位置。BL2入口关键路径bl2/bl2_main.c里的bl2_main()它会先做平台初始化再调用load_and_run_bl31()之类的函数。主流ATF代码里BL2通过bl2_plat_handle_post_image_load钩子来处理每个镜像加载完之后的额外操作比如安全认证。你把整个FIP包Firmware Image Package里的镜像依次解包、验证、放到指定内存地址最后把控制权交给BL31。BL31入口关键路径bl31/bl31_main.c里的bl31_main()它会调用bl31_early_platform_setup2()、bl31_plat_arch_setup()、bl31_platform_setup()逐个初始化平台然后进入bl31_lib_init()把EL3的运行时服务注册好PSCI、SIP等最后执行el3_exit()以SMC返回的方式把控制权交给BL33。另一个值得读的关键文件是lib/el3_runtime/aarch64/context.S这里定义了CPU上下文的保存和恢复。EL3要响应来自普通世界的SMC就必须在陷入异常时保存调用者的寄存器现场处理完再恢复。代码里能看到大量stp x0, x1, [sp, #...]的存取操作配合include/lib/el3_runtime/aarch64/context.h里的cpu_context_t结构体就能理解BL31的世界切换机制。2.2 runtime services框架SMC分发器是怎么决定谁来处理这个请求ATF的运行时服务机制是整个BL31的精髓。你觉得PSCIPower State Coordination Interface是ATF的一部分这个理解对但不够准确。PSCI是Arm定义的电源管理接口规范ATF只是其中一个实现。BL31运行时的核心数据结构是一张服务表每个服务有一个OEM ID、服务ID、调用入口。在源码里看services/std_svc/std_svc.c里面定义了一个数组把服务ID和对应的handler绑定。BL31收到SMC异常后会调用handle_smc()它根据SMC的Function ID的Bit[31]来判断是Fast Call还是Yielding Call然后根据Bit[24:16]之类的字段查找对应的服务。分发逻辑在services/std_svc/std_svc.c与bl31/bl31_main.c的初始化逻辑之间打通。我调试时经常打印FIDFunction ID比如标准服务的PSCI_CPU_ON_AARCH64的FID是0xC4000003如果看到这个值进入PSCI handler就知道系统在请求CPU启动。所有系统的电源状态切换、CPU hotplug、系统重启最终都是通过SMC触发BL31的PSCI实现再由PSCI调用GIC、时钟、功耗管理等底层驱动完成这个过程在移植时很容易出错。2.3 安全世界的中转站从ERRATA到EL3 SPMC的扩展视野除了标准BL1-BL31流程ATF源码里还包含大量扩展组件。lib/cpus/目录下是各厂商CPU的勘误表ERRATA处理代码进入BL31时会根据MIDRMain ID Register匹配CPU型号打入补丁。做移植时如果遇到某些诡异崩溃第一反应应该想到是不是漏了某个勘误尤其是新出的Core。services/spmd/和services/spmc/则是与Arm CCAConfidential Compute Architecture相关的组件支持在EL3直接运行SPMC管理安全分区。如果你的平台用不到这些编译时可以排除。但如果你的芯片是Armv9新平台开始规划CCA方案ATF这部分代码迟早要啃。从启动链到runtime框架ATF源码的阅读路线应当是入口—平台钩子—服务分发三条线并行。入口告诉你做什么平台钩子告诉你该适配什么服务分发告诉你运行时的调用路径。清楚了这三条线移植时你才不会被几百个源文件淹没。3. 安全固件工程审计不是能开机就算安全很多团队做ATF移植点亮Linux就算大功告成从不做安全审计。这等于你家大门装了把锁但锁芯是塑料的。ATF作为系统最高权限固件一旦被攻破整个系统沦陷。以下是我做固件安全审计时最关注的几个维度。3.1 信任链与镜像验证从ROTPK到FIP包的完整校验流程ATF支持Trusted Board BootTBB这也是绝大多数商用量产平台必须开启的功能。TBB的基本思路是建立一条信任链信任根是芯片内部的ROTPKRoot of Trust Public Key它被烧写在eFuse或一次性可编程存储里只有芯片厂商能写入。启动时每个BL镜像都附带证书和签名。BL1内置ROTPK的哈希加载BL2时验证BL2证书BL2再去验证BL31/BL32/BL33的证书。这条链从根到叶子逐级校验任何一级被篡改都会导致启动终止。审计时我一般会对着tools/cert_create/目录去检查证书生成流程再看看plat/common/tbbr/里平台如何实现认证。重点是看平台的auth_mod指针有没有把非安全世界的镜像排除在验证外以及ROTPK的存储实现是否真正一次性写入。有些开发板为方便调试会把认证开关直接关掉——这在量产板卡上是重大安全隐患我审计到过好几例。3.2 权限模型与内存隔离为什么ATF的数据区不能被普通世界碰ATF运行在EL3它执行时使用的内存必须对普通世界完全不可见否则普通世界的恶意代码就能直接改写BL31的代码或数据结构然后等待系统调用实现提权。内存隔离用Arm的TrustZone机制实现主要是TZASCTrustZone Address Space Controller和TZMATrustZone Memory Adapter。BL31在bl31_plat_arch_setup()里会把安全内存区域配置好普通世界访问这些区域会直接引发异常。审计中我经常发现三类问题一类是调试用的内存映射没有删除把BL31的调试串口缓冲区留在了普通世界可见的地址一类是MMU的Translation Table配置错误某个安全区域被错误标记为Non-Secure还有一类是TZASC配置后某个地址区域的边界算错导致相邻的普通世界内存被安全世界占用或反过来。我在一篇内部审计报告里专门写过一个检查清单这里分享几个最核心的检查点检查BL31的xlat_table映射中所有BL31代码和数据区域是否设置MT_SECURE属性。检查TZASC的region地址是否覆盖全部保留给安全固件/TrustZone的内存。检查BL33传入的memory map是否把安全内存区域错误暴露给普通世界。检查运行时服务handler中所有来自普通世界的指针是否都做了地址范围和权限验证。3.3 SMC接口审计来自普通世界的每个请求都要被审问BL31通过SMC对外提供服务这是普通世界唯一能触达安全世界的通道。如果服务接口处理不严谨就会成为攻击面。我审计时关注两个重点参数校验和边界检查。很多ATF的SMC handler会从x1~x7接收参数标准的Arm SMCCC规范要求对参数做严格校验。比如一个内存读写服务如果传入的地址不属于调用者那就能越权访问安全内存一个设置电源状态的接口如果状态值非法可能导致系统崩溃或死锁。以PSCI handler为例services/std_svc/psci/psci_main.c里的psci_cpu_on()会校验target CPU的ID是否合法检查上下文是否已经准备同时检查入口地址是否在合理范围内。审计时我会逐个服务接口做恶意参数注入测试看是否有断言被触发或异常泄露信息。一个容易被忽略但极其危险的隐患是handler里的临时缓冲区。有些实现把SMC调用方传入的数据直接memcpy到栈上缓冲区却没有检查长度这就是经典的栈溢出漏洞。ATF本身对这块管控严格但你自己的平台代码、你自己实现的SIPSilicon Provider服务往往是薄弱环节。3.4 密钥管理与证书生成烧进eFuse之前要想清楚的几件事密钥管理是安全审计里最敏感的部分。TBB使用的密钥分为几层ROTPK、BL31密钥、BL32密钥、BL33密钥、Non-Trusted世界密钥。生产时这些密钥通常在HSM硬件安全模块里生成私钥永远不会出现在构建机器上。我见过一些团队为了省事直接用开发仓库里自带的测试密钥烧进样机。这等于把整栋楼的钥匙插在门上。审计建议至少做到生产环境使用独立的PKI体系测试密钥只用于开发板ROTPK的烧录要在产线上用专用工具完成不能通过系统应用下发所有私钥存储遵循最小权限原则只有授权工程师能接触。还有一个细节容易被忽略fiptool打包时会把证书和镜像混在一个FIP包里如果FIP包本身存储在不安全的分区比如普通世界的文件系统里攻击者虽然不能伪造签名但可以替换整个FIP——所以还要保证FIP所在分区的完整性校验最好放在安全存储或受保护的启动设备里。4. 平台移植落地从零适配一块新板子的完整路径这一节我们讲实操。我以一个虚拟到的nova2160平台为例实际芯片名不写了避免对号入座梳理移植ATF的标准路径。注意不同版本的ATF对平台目录结构有差异我这里以较新的主线行为主实际操作时以你的repo为准。4.1 创建平台目录那些看起来一样的Makefile其实暗藏玄机ATF的平台代码统一放在plat/厂商/平台名/目录下。第一步是参考一个已有平台我建议用plat/qemu或plat/fvp做原型因为它们结构清晰、依赖最少复制一份出来改名。平台目录里最关键的是platform.mk它定义了PLAT_BL31_SOURCES编译BL31需要包含哪些源文件。PLAT_BL_COMMON_SOURCESBL1/BL2/BL31通用的平台源文件。BL31_SOURCES额外的BL31源文件。$(eval $(call add_define,xxx))平台自定义的宏定义。这里最容易犯的错是漏掉源文件。比如你实现了plat_get_syscnt_freq2但忘了加plat_common.c编译时链接失败然后陷入为什么别人能编过我不能的困境。我的建议是新平台尽量以参考平台的platform.mk为基底逐项替换不要自己从头写。平台头文件include/plat/arm/common/arm_def.h如果基于Arm公共代码或自定义的platform_def.h里要定义一堆地址宏比如BL31_BASE、BL31_SIZE、BL2_BASE、BL2_SIZE、PLAT_PHY_ADDR_SPACE_SIZE等。这些宏直接决定镜像放在内存的哪个位置安排不合理会导致镜像互相覆盖。4.2 平台钩子函数十几个必须实现的回调里哪个最容易被忽略ATF定义了一套平台抽象层每个平台必须实现若干钩子。以下是最核心的几组早期初始化bl31_early_platform_setup2()串口、电源、时钟初始化基本都在这。架构初始化bl31_plat_arch_setup()负责配置MMU和内存映射。平台设置bl31_platform_setup()做GIC、系统计数器、TZASC等外设配置。PSCI操作plat_psci_ops包括cpu_on、cpu_off、system_reset、system_off等。运行时服务plat_setup_psci_ops()、plat_get_syscnt_freq2()、plat_get_arm_gic_driver_data()。我最常看到被忽略的坑是**plat_get_syscnt_freq2**。很多平台没有实现它或者返回值不对。这个函数让BL31知道系统计数器频率用来计算时间戳。如果返回值是0或错误值PSCI的SYSTEM_SUSPEND、CPU_SUSPEND等需要时间计算的功能全都会异常。而且这个函数的调用时机很微妙BL31在运行时服务初始化阶段会调用它如果此时返回值错误后续很多逻辑都会带着错误参数跑。另一个高频坑是串口。很多人以为串口只在调试时用跟安全无关。错ATF早期启动离不开串口而且串口的时钟源配置、波特率计算如果不对你会连一行log都看不到。我建议新平台移植第一步就搞定串口从BL1的console_init开始验证把日志打通后再往下走。4.3 中断控制器与系统计数器CPU hotplug和系统休眠跑不跑得通全看这里BL31管理着整个系统的电源状态而电源状态管理与中断控制器强相关。ATF里GICGeneric Interrupt Controller的驱动在drivers/arm/gic/平台需要在platform_def.h里定义GICD_BASE和GICC_BASEGICv2或GICD_BASE和GICR_BASEGICv3。我踩过一个坑某平台用的GICv3但参考代码写的是GICv2的初始化和中断配置导致BL31跳转后一开中断就死机。后来查清楚GICv3的Redistributor基地址没配CPU接口的SGI无法送达。审计时也常有这类问题——中断控制器不匹配或者配置错误导致安全中断无法正确分发整个系统稳定性受到严重影响。系统计数器System Counter是另一个关键模块。它给系统提供一个单调递增的时间基准BL31用它计算PSCI的超时时间。Arm架构的系统计数器频率通常在1MHz到100MHz之间CNTFRQ_EL0寄存器保存当前频率。ATF的plat_get_syscnt_freq2返回的值用于初始化计数器如果频率配置不对休眠唤醒后的时间计算会全错可能导致唤醒流程死锁。4.4 编译与烧录GCC版本、fiptool打包和镜像地址的边界条件编译ATF相对简单但有几个坑值得一提。工具链建议用aarch64-none-elf-裸机版本或者aarch64-linux-gnu-也可以但版本不要太老。我遇到过老版本GCC编译优化导致BL31异常跳转的案例升级到较新的Linaro工具链后问题消失。编译命令一般是make PLATnova2160 DEBUG1 V1 bl31DEBUG1会使用-O0并打开所有日志级别调试很方便量产版本用DEBUG0配合LOG_LEVELNOTICE降低日志输出。V1可以输出完整编译命令排查include路径问题时很有用。编译完成后你会得到build/nova2160/debug/bl31.bin。要生成完整的FIP镜像需要先把BL2、BL31、BL32如果有、BL33U-Boot打包make PLATnova2160 DEBUG1 fip BL33path/to/u-boot.bin它会调用tools/fiptool/fiptool把所有镜像打包进fip.bin。如果你开启了TBB打包前要先用tools/cert_create/cert_create生成证书然后再让fiptool把证书和镜像一起打进去。在连接脚本和地址安排上platform_def.h里BL31_BASE的选择必须避开BL2正在使用的内存。常见做法是把BL2放在SRAMBL31放在DDR高地址的安全区域BL32放在独立的安全DRAM。地址冲突的典型症状是BL31跳转后跑飞或者U-Boot启动后内存校验失败。这个坑排查起来极其费时间最好在移植初期就把内存布局图画清楚。4.5 QEMU/FVP上先跑通为什么我强烈建议先虚拟化验证再做真机如果你手头没有真实硬件或者真机调试困难先用QEMU或FVPFixed Virtual Platform验证是有很大价值的。ATF官方对QEMU/FVP的支持很完善make PLATqemu debug bl31直接能编出可运行的镜像配合QEMU的-machine virt参数就能启动。虚拟平台的好处是能打断点、单步跟踪、随时dump内存排查移植逻辑问题效率非常高。我一般先把平台代码在FVP上跑通再上真机。很多平台相关的Bug比如函数指针错误、结构体配置错位在虚拟平台上一跑就露馅。但要注意虚拟平台不能代替真机验证硬件的电源、时钟和DDR初始化时序。QEMU里CPU一按就能开真机里可能要等PMIC慢慢上电。5. 移植调试中的高频坑我踩过的和帮别人排过的最后一个部分集中写移植和调试过程中大概率遇到的高频问题每个坑背后都是血泪教训。5.1 启动Log卡在BL1或BL2先查串口时钟和DDR初始化启动Log是调试的第一线索但如果它卡在早期阶段排查思路要分情况。卡在BL1大概率是串口时钟没配好或者BL1的入口地址不对。卡在BL2大概率是DDR初始化失败——BL2运行在SRAM里还好但它后面的BL31、BL33都要在DDR里跑如果DDR没初始化好BL2加载镜像后一跳到DDR就死路一条。DDR初始化的坑通常是时序参数和PHY校准很多时候你的DDR频率配得太激进跑不到系统稳定。我建议在BL2里加日志逐步打印关键初始化步骤定位到具体是哪个阶段卡住。另外注意区分DDR控制器初始化失败和DDR地址被映射错误两种不同情况前者看寄存器后者看MMU映射表。5.2 BL31跳转瞬间崩溃MMU配置、异常向量表和栈是三大元凶BL31初始化完成后要el3_exit()跳转到BL33这是事故高发区。我遇到的崩溃原因基本集中在三处首先是MMU配置错误。BL31在bl31_plat_arch_setup()里建立页表。如果页表没找到对应的物理内存区域或者访问权限设置错误一跳转就发异常。我当时排查一个BL31一跑就进Undefined Instruction的问题最后发现是Translation Table的level配少了导致虚拟地址范围重叠。其次是异常向量表没对齐。Arm要求异常向量表按平台要求对齐到特定边界ATF启动时会设置VBAR_EL3。如果向量表地址没对齐任何异常都会跳到一个错误的地方表现为什么都没干就死机。这类问题用JTAG调试器查看PC值最容易发现。第三是栈指针错误。BL31每个CPU都有独立的栈栈顶地址在入口汇编里设置。如果栈大小定义太小压栈时覆盖了别的数据结构也会导致神秘崩溃。这种问题在DEBUG1不优化时可能不出现一旦开-O2优化才暴露排查起来极其头疼。5.3 PSCI调用失败当U-Boot和内核要开第二个核时你在哪里翻车U-Boot起来后它要启动Linux的其它CPU核这个过程会触发PSCI的CPU_ON调用。如果BL31的PSCI实现有问题Linux启动多核时会随机死机或报CPU stalled。我记得有一个平台的PSCI CPU_ON一直不工作查了几天发现是GIC的SGI中断配置不对BL31发出的唤醒中断根本没送达目标核。这个问题的排查方法是在BL31的psci_cpu_on里加日志确认是否收到调用然后在目标核的启动入口处加日志确认核是否真的被唤醒最后用JTAG查看目标核的PC有没有跳到BL31指定的入口地址。还有一种情况是CPU密勒MCS等低功耗模式配置错误导致CPU OFF后无法恢复。这类问题通常与硬件功耗管理固件的协作方式有关需要阅读SoC的电源管理手册确认BL31调用的PSCI底层接口是否符合设计。5.4 TBB开启后无法启动证书、密钥与镜像版本的三角恋很多团队在开发阶段不开TBB调试方便到量产前才打开。这时最常见的问题是签名不匹配。比如BL2的证书是用旧证书生成的而你换了新的BL31但没有重新生成证书又比如ROTPK烧错了位置导致BL1验证BL2失败。ATF启动时会打印认证失败的具体原因定位方向是先确认证书链是否正确再确认ROTPK的存储值是否与构建时预期一致最后确认镜像哈希是否与FIP包一致。我还遇到过一种状况QEMU上TBB可以正常启动真机上就不行。排查后发现是eFuse的ROTPK读取时序需要平台驱动支持而平台代码里没实现这个读取接口。这个问题在开发板上可以用烧录工具绕过但量产前必须解决。5.5 经验小结给准备做ATF移植的工程师一份动手前的自检清单按重要程度排一个清单作为你动手前的检查项确认芯片的启动流程BL1是否由ROM固化还是ATF的BL1也需要参与。确认内存布局BL1、BL2、BL31、BL32、BL33各自放在哪里能否互相覆盖。确认串口初始化时钟源、频率、引脚复用第一行日志能不能出来。确认GIC版本v2还是v3基地址、Redistributor地址是否正确。确认系统计数器频率CPU频率和计数器频率是两回事不要混为一谈。确认TBB策略量产前先设计好证书和密钥体系不要等到最后才临时抱佛脚。确认多核启动策略哪些核由BL31直接接管哪些核由PSCI动态启动。这七项如果能在动手前梳理清楚移植周期至少能缩短三分之一。写在最后我看过的ATF移植项目里做得快的不是代码写得最熟练的人而是对启动链理解最透彻的人。BL1到BL33的接力、EL3常驻runtime服务、SMC分发的逻辑这些底层概念一旦通了后面所有平台适配都只是按要求填函数的问题。反过来如果一上来就埋头改Makefile大概率会在地址冲突、MMU配置和GIC版本上耗掉几周时间。希望这篇基于源码和实战经验的拆解能帮你少走我在这些坑里走过的弯路也让更多人意识到Trusted Firmware不只是拿来编译一下的东西它值得被认真对待。
返回列表