ARTICLE DETAIL

资讯详情

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

深入ATF可信固件:从EL3架构到平台移植避坑指南

深入ATF可信固件:从EL3架构到平台移植避坑指南 咱们做嵌入式或者系统级开发的手里只要过了几个项目基本都会撞上ATF这个词。搞安卓的内核版本升级要碰它做服务器国产化适配要碰它连做物联网安全方案选型的时候卖芯片的FAE也会拿ATF出来当安全卖点。但说实话很多人对ATF的印象停留在“uboot之后、kernel之前跑的那段神秘代码”真要出问题了要么全网搜报错要么对着文档干瞪眼。这篇文章我不打算给你复述官方文档也不做那种注释级别的行行解读。我从实际工程落地的角度把ATF的架构骨架、源码里真正影响你移植工作的关键逻辑、以及我在不同平台Cortex-A53、A72、A76都有上移植和调试时踩过的坑串起来讲。你看完不一定能立刻手写一个BL31但至少对“ATF到底在干什么”“出了问题该往源码里哪里看”“改平台代码时哪些宏和函数是绕不开的”会有一个清晰的地图。1. 先搞懂ATF在整条启动链里扮演的角色很多初学者容易把ATF和U-Boot搞混觉得都是“引导程序”。这个认知偏差在排查问题的时候特别要命。你要明白ATF的定位不是一个通用的bootloader它是ARM为了在EL3Exception Level 3这个最高特权级别下运行安全监控逻辑而专门设计的一套固件框架。它的核心任务有三个系统初始化的最小集、安全世界与非安全世界的切换仲裁、以及运行时服务的注册与转发。1.1 从BL1到BL33每一级固件各管一段ATF的启动流程是一级一级往上递进的每一级都有明确的职责边界理解这个边界是你后面看懂任何ATF日志的基础。BL1Boot Loader stage 1这是上电后CPU在EL3执行的第一段代码。它通常是固化在ROM里的或者是BootROM从外部加载到SRAM里的。BL1的活儿非常纯粹初始化最基本的存储控制器把BL2的镜像从flash里搬到SRAM然后跳过去。它为整个信任链提供了起点这一级的物理攻击防护通常由芯片内部的OTPOne-Time Programmable和ROM校验机制完成你普通开发者基本碰不到它但要知道它的存在。BL2Boot Loader stage 2运行在EL3但是是可信固件的一部分。它的主要任务是初始化DDR内存、加载后续的镜像到内存里包括BL31、BL32可选、BL33并且负责验证这些镜像的签名。在实际平台里BL2的代码改动空间比较大尤其是DDR初始化参数如果你的板子老是死在内存训练阶段那八成问题出在BL2阶段。BL31Boot Loader stage 3 Runtime这块是ATF的运行时核心也是你调试最多的部分。BL31常驻在内存里它负责在EL3处理安全监控调用SMC管理中断路由提供PSCI电源状态协调接口服务。整个操作系统运行期间BL31都活着像个门卫一样把守着安全世界和非安全世界的大门。BL32Trusted OS / OP-TEE这是可选的跑一个可信操作系统比如OP-TEE。它运行在S-EL1。如果你不需要厂商的DRM、安全支付等强隔离功能可以不编译它直接跳过。BL33通常就是U-Boot或者UEFI它就是非安全世界的引导程序运行在EL2或者EL1。对U-Boot来说它根本不感知前面有BL1、BL31它只知道自己从BL31手里接管了CPU继续初始化硬件然后引导内核。这里有一个特别重要的逻辑ARMv8的异常级别模型是整个ATF设计的地基。EL3是最高的特权级EL2是虚拟化扩展EL1是操作系统内核EL0是用户态。ATF的代码就趴在EL3上像监控室的安保一样盯着所有异常级别之间的切换。1.2 为什么需要EL3这么个“盐碱地”来放固件我见过有人问直接让U-Boot在EL3跑完引导内核不就行了干嘛非得搞ATF这个问题的答案得从TrustZone说起。TrustZone把系统硬件资源分成了安全世界和非安全世界安全世界里跑的可信应用不能直接被普通世界访问。这个硬件隔离是CPU和总线层面的不是软件层面靠“自觉”。既然要隔离就必须有一个“比操作系统更高”的权限主体来管理两个世界之间的跳转和共享资源的访问授权。这个主体不能是普通世界的软件因为普通世界自己就是被监控的对象。ATF在EL3正好承担这个职责。普通世界内核里的TrustZone驱动想调用安全世界的某个功能时会把参数放进寄存器里然后执行一条SMCSecure Monitor Call指令CPU会直接陷入EL3由BL31的异常处理代码来决定这个请求是否合法、应该转给哪个安全服务处理。这个过程完全不等同于普通世界的软件异常它是一个体系结构层面的“硬件断点”。你理解了EL3的角色后面的源码阅读才会有方向感。你会发现在源码里对异常向量表、SMC调用号的分发逻辑占用很大篇幅这些都是作为“仲裁者”必须要承担的职责。2. 源码主目录结构与核心模块的精读地图面对ATF仓库现在叫Trusted Firmware-A很少有人愿意一个个文件读。正确的战略是“按图索骥”你需要知道自己要干什么然后直奔对应的目录。整个仓库的编排逻辑很清楚平台相关代码、SoC相关代码、框架通用代码三级分离。2.1 plat/、drivers/、lib/ 这三个目录的权力边界plat/这是你把板子适配进来以后最常动的地方。里面是开发板相关的实现比如QEMU平台在plat/qemu/树莓派在plat/rpi/各种厂商的SoC也都有对应目录。这个目录下面会有include/platform_def.h这个头文件定义了整个平台几乎所有的宏参数——比如UART的基地址、内存的起止地址、GIC通用中断控制器的配置、BL31的加载地址。我把platform_def.h称为“ATF移植的金钥匙”你改板子八成都是从这儿开始的。drivers/存放各种外设驱动比如串口drivers/uart、GIC中断控制器drivers/arm/gic、MMC存储控制器、Cryptocell加解密引擎等。这些驱动在ATF内部是被BL1或BL2阶段使用的因为此时操作系统还没起来只能靠这些“裸”驱动去做最基础的硬件初始化。lib/存放一些通用的代码库比如el3_runtimeEL3运行时的核心逻辑、psciPSCI标准的实现框架、xlat_tables_v2MMU页表管理。这里面的代码大部分是不动脑子的那种通用模板但你要理解它们的调用逻辑。记住一个经验厂商给你提供的ATF包通常改的就是plat/目录下的文件和include/plat/下的头文件。如果你拿到一份第三方BSP对比原版ATF发现差异几乎都在这两个地方那说明移植思路基本可行。2.2 值得细读的启动与运行时路径如果你想精读源码我建议优先读这几个文件它们是ATF内部逻辑的骨架bl31/bl31_main.c这是BL31的C语言入口bl31_main()函数会初始化运行环境、注册各种运行时服务、然后进入一个死循环等待事件触发。读这个文件能让你知道BL31在PSI阶段之前都初始化了什么东西。lib/el3_runtime/aarch64/context_mgmt.c这文件负责管理CPU上下文。普通世界陷入EL3时CPU寄存器会被保存起来当BL31决定回到普通世界时又得恢复那些寄存器。context_mgmt.c就是干这个“存包取包”的活儿。你要是调试过“从内核调SMC以后卡死”的问题大概率要回到这里来查上下文是否被正确保存和恢复。lib/psci/psci_main.cPSCI的入口框架内核里的cpu_off、cpu_on、system_reboot等电源管理命令最终都会从内核通过SMC指令走到这里。你移植板子时如果发现CPU无法关闭或唤醒问题几乎都出在这条调用链路的某个环节。读这些文件不需要逐行看汇编重点看它的调用关系、函数注释以及结构体如何使用这样效率最高。3. 安全固件工程审计到底审什么“工程审计”这个词听起来很高级但在实际项目里其实就是拿着源码和配置做体检。这个体检至少包含三个维度信任根与启动链路的完整性、运行时服务暴露面的最小化、以及内存隔离与权限配置的合理性。下面我拆开讲这些都直接影响你的产品能不能过安全评估或者会不会在实战中被攻破。3.1 可信启动链每一棒都验签了吗可信启动的核心思路是“逐级验证环环相扣”像接力赛一样每一棒都要检查上一棒给过来的东西靠不靠谱。ATF落地的时候你在platform_def.h里会看到类似ARM_FIP_SRC的配置这指的是FIPFirmware Image Package文件里存放了BL2/BL31/BL33的镜像。BL1从FIP里读出BL2时必须用OTP里烧录的公钥去验BL2的签名BL2去加载BL31、BL33时也要做同样的验签动作。我在实际工程里见过一个典型的失误为了加快开机速度把BL2阶段的签名校验直接屏蔽了用#define跳过验签。虽然在开发板上跑得很欢但产品一旦流片量产任何人都可以自己构造一个FIP刷进去直接绕过所有高权限固件。审计时第一件事就是确认TRUSTED_BOOT_BOARD相关的宏有没有打开并且确认公钥和OTP的烧录流程是不是安全的。这是最基础的底线。3.2 运行时服务的攻击面与SMC调用号审计BL31长驻EL3它对外暴露的主要接口就是SMC调用号。攻击者或者恶意App可能会从非安全世界构造各种SMC参数试图让EL3的代码做出越权行为。审计要做的就是去掉所有不必要的运行时服务。ATF源码里通过DECLARE_RT_SVC()宏来注册服务注册之后服务才能在SMC分发表里被找到。如果你没用可信操作系统那就别把OP-TEE服务编进来如果不用RASReliability, Availability and Serviceability就关掉RAS相关的服务PSCI服务是跑Linux必须的但也要检查里面接口是否都符合规范。真实的攻击面审计拿着rt_svc_descs这个结构体数组和芯片的数据手册对照逐个确认哪些服务暴露了、哪些功能其实没用到然后干净利落地移除。3.3 页表和内存权限配置细节里全是魔鬼ATF自己的代码和数据结构都在内存里跑它通过MMU页表给这些内存区域设置了极细粒度的权限。比如BL31的代码段是只读可执行的数据段是读写不可执行的设备内存映射成Device-nGnRnE属性外设寄存器区域绝对不能缓存。审计时要重点看xlat_tables_v2的映射配置是否存在某个本该配置成设备内存的区域被配成了普通可缓存内存或者某段关键固件代码区被配成了可写的这种问题一旦出现就没法和别人争论“硬件隔离是安全的”这件事了。4. 平台移植落地的完整工程步骤我这里给你一套我磨合过很多次的ATF移植步骤。它不是唯一正确答案但能帮你少踩一半的坑。4.1 拿到一块新板子第一步该干嘛第一步永远不是写代码而是确认参考设计。如果你的SoC有官方评估板EVB并且官方发布了对应的ATF源码包那你的工作80%是基于EVB的配置做修改而不是从零开始搭。从零写一个plat/目录下的平台代码工作量非常巨大很多团队一开始低估了最后都付出了代价。具体检查顺序是确认芯片型号和ARM核心型号这决定你用Cortex-A53、A72还是A76的流水线配置。拿到该芯片的TRMTechnical Reference Manual尤其是内存映射表、UART控制器、GIC、定时器的基地址。对照EVB的原理图确认你的板子裁剪了哪些外设、改了哪些GPIO。有了这三个信息你才敢去改platform_def.h里的宏否则全靠猜就是在埋雷。4.2 修改内置的基础配置宏以Cortex-A53平台举例platform_def.h里往往有一堆你需要自定义的东西PLAT_PHY_ADDR_SPACE_SIZE定义物理地址空间大小如果你的板子最大支持4GB内存这里要和内存控制器寄存器一致。PLAT_MAX_OFF_STATE和PLAT_MAX_RET_STATE这决定PSCI的CPU空闲状态怎么暴露给内核。设错了可能导致内核在idle的时候唤醒不了。MMAP_UART0_BASE、MMAP_UART0_CLK_IN_HZ串口调试的基础ATF早期日志全靠它。时钟频率算错最典型的表现就是串口输出乱码。ARM_GIC_BASE和ARM_GICD_BASEGIC中断控制器的地址做中断路由必须对。每次改完宏都要重新编译并在板子上验证。我见过有人一口气改了30个宏结果板子完全起不来都不知道从哪里开始查。工程上最稳的做法是“一次只改一批验证一批”。4.3 编译和部署的常规流程ATF用的是make体系最烦人的是编译参数多。我的习惯是先设置CROSS_COMPILE环境变量指定arm交叉编译工具链然后按下面的命令编译make CROSS_COMPILEaarch64-linux-gnu- PLATqemu DEBUG1 \ BL33../u-boot/u-boot.bin fip这个命令里PLAT指定平台DEBUG1会带上调试符号方便你跟踪执行路径。BL33指向U-Boot的镜像fip目标会把BL31和BL33打包成一个FIP镜像。如果只需要编译单个BL31镜像用于调试不打包FIP可以直接make CROSS_COMPILEaarch64-linux-gnu- PLATqemu DEBUG1 bl31编译完看到build/qemu/debug/bl31.bin就成功了。如果你编译时报错找不到lib/下的某些头文件几乎都是交叉编译工具链版本和源码不匹配导致的换个官方推荐的ARM工具链版本通常能解决。4.4 启动日志的解读方法ATF启动日志的详细程度取决于LOG_LEVEL这个宏。LOG_LEVEL40是VERBOSE模式会输出巨多调试信息生产环境默认是NOTICE级别20。移植阶段我建议直接开到LOG_LEVEL40。make CROSS_COMPILEaarch64-linux-gnu- PLATqemu DEBUG1 LOG_LEVEL40 bl31日志里最常见的几个关键字NOTICE: BL31: v2.8(release):表示BL31版本和信息确认加载的时间戳。ERROR:后面跟着的报错定位几乎都靠它。Running image at ...这里能看到镜像加载地址如果地址和你的链接脚本对不上大概率是访问的死机原因。当时的实际经验是当你打开VERBOSE日志以后BL31内部每个阶段的初始化函数都会打印你基本能肉眼追踪到哪一步挂了。这比云里雾里地从JTAG抓寄存器靠谱得多。5. 我在实际项目中踩过的三个典型“天坑”这部分讲几个我真实遇到过的案例每个案例背后都对应着一类问题比单纯的说明书有意义。5.1 U-Boot从BL31拿到的是“假”的MMU状态现象BL31启动U-Boot后U-Boot在串口打印很欢快但一执行到开启缓存相关逻辑就死机。排查链路一开始我想当然地认为是U-Boot的缓存配置有问题但在QEMU和另外一块板子上U-Boot是正常的。然后就翻ATF代码发现bl31_main()里配置的MMU缓存策略是“device memory region非缓存”而我用的那块板子在BL31阶段把某个原子操作寄存器也映射成了普通内存属性。结果就是缓存一致性出了问题U-Boot里访问这个自旋锁寄存器时读到了脏数据导致死锁。最终原因ATF给普通世界设置的初始MMU页表属性过于宽松把某些需要严格设备语义的地址段覆盖到了普通可缓存内存里。解决办法是检查页表配置把该地址段改成MT_DEVICE属性。这给我的教训是U-Boot收到的初始运行环境不是“干净的裸机环境”它里面藏了很多ATF的影子排查问题不能只看U-Boot自己。5.2 CPU挂起后系统无法唤醒其实是唤醒地址总线错位现象内核执行cpu_suspend进入idle后唤醒的瞬间偶尔会复位或死机。排查链路内核PSCI请求CPU_SUSPEND的时候会把自己在内存里的resume地址传给BL31。BL31保存上下文之后把CPU关掉。唤醒的时候BL31要把PC指针指到之前保存的resume地址恢复上下文然后跳回去。我当时的板子一直偶发复位查了好几天后来加了VERBOSE日志发现BL31在唤醒时加载的地址根本不对——低12位地址总被某些位运算清掉了。最终原因PSCI调用传参时地址的高位被我的平台移植代码多移位了一位导致地址错了4KB对齐。最搞笑的是这个bug在大部分时间CPU是唤醒成功的因为恰巧合法的地址落在内核的某个函数附近误打误撞还能跑。后来把所有唤醒地址都统一用ALIGN()处理问题彻底解决。5.3 中断被路由到了错误世界里EL3和内核反复踢皮球现象GPIO中断频繁丢失系统响应很慢。排查链路ARM GIC允许把中断指定到安全世界或非安全世界。如果某外设的中断被配置成只能路由到安全世界但安全世界里又没有处理程序BL31会看到这个中断然后尝试把中断转给普通世界。由于配置逻辑不清普通世界可能不认这个中断号两个世界就来回踢皮球中断就一直得不到处理。最终原因我的板子平台代码里有一处关于GICD_IGROUPR的初始化残留。某些想分配给普通世界的中断被错误地设置成了Group0安全中断。修完之后把所有外设中断属性重新捋了一遍一切恢复正常。这个坑说明中断分组配置的审计一定要结合整个系统的构建说明来做不能只看某一段代码的配置就下结论。6. 移植过程中我用着顺手的几个工具与验证技巧除了ATF源码本身一些外围工具和验证手段能极大提升排错效率。6.1 交叉编译工具链选择与链接脚本ATF对交叉编译工具链有版本要求我记得有些老版本ATF配新的GCC会出AT_*汇编语法兼容问题官方推荐用gcc-arm-9.2-2019.12-x86_64-aarch64-none-linux-gnu这种Linaro风格的工具链或者直接用ARM官方维护的arm-none-eabi-gcc。写链接脚本时要重点确认BL31_BASE的地址不能和其他镜像重叠。6.2 用QEMU搭一套软硬件一致的仿真环境QEMU的virt平台支持ATF而且社区维护得很好。强烈建议你在板子到手之前先在QEMU上跑通ATFU-BootLinux启动再用一套相同的代码上板子。qemu-system-aarch64 -machine virt -cpu cortex-a53 -nographic \ -bios bl1.bin -append ...如果你PC上装的是ARM架构的虚拟机QEMU的性能瓶颈小很多调试体验会更好。跑起来之后你可以用QEMU monitor的xp物理内存查看命令检查BL31镜像是否被正确加载。6.3 用JTAG调试器直接打断点看EL3状态ATF作为安全固件普通调试器默认进不了EL3。如果你想调试BL31除了用openocd这些工具的口还得留意芯片有没有开放调试权限。很多SoC需要先烧写调试密钥否则JTAG上电是被锁死的。这一块我建议在量产板阶段就认真规划调试策略否则后期定位问题会非常痛苦。7. 关于ATF后续演进与踩坑心态的一些个人思考最后聊一点宏观层面的。ATF本身在持续演进这几年Cortex-X系列核心对PPA性能、功耗、面积的要求越来越高ATF里关于电源管理、DVFS的优化代码越来越多。平台移植工作的复杂度只会不断上升。我在做审计的时候习惯把ATF当成一台“持续运行在EL3的小型操作系统”来看。它有自己的异常处理、任务调度、内存管理、甚至中断管理只不过这些东西都服务于“隔离与可信”这一个目标。基于这种理解每次看到某些厂商为了快速适配而阉割掉安全特性我都会觉得不妥。分享一个非常实际的建议无论你的产品最终需不需要过任何安全认证都尽量保持ATF默认的安全配置不动尤其是TRUSTED_BOOT_BOARD的宏。哪怕你只在开发阶段把验签关了一旦忘了开回来流片后就是安全漏洞。最后再提一个我自己长期受用的小技巧在ATF的plat/common里加自己的打印函数别直接改官方代码单独建一个debug_helpers.c来放你所有的临时调试工具。这样即使ATF版本升级你也可以直接把自己的调试文件搬到新仓库里不会因为和官方代码冲突而痛不欲生。希望这篇脉络梳理对你的ATF之旅有实际帮助。以后你被Linux内核里的神秘死机问题逼到想放弃的时候可以回头看看是BL31哪个环节给了你那么大惊喜。
返回列表