尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ARM可信固件ATF架构解析:BL31启动流程、安全机制与平台移植实践

ARM可信固件ATF架构解析:BL31启动流程、安全机制与平台移植实践 最近在调一块ARMv8.2核心板要把安全启动、TEE和Linux引导一条龙贯通被迫把Arm-Trusted-FirmwareATF的源码从启动入口到平台适配翻了个底朝天。网上关于ATF的讨论大多停留在“怎么建个plat目录”的阶段真正能把BL1/BL2/BL31链路、信任根设计、SMC分发和平台移植串成体系讲透的并不多。这篇文章就用源码审计的视角把ATF的架构全景、安全机制和落地移植一起拆开适合刚接手安全固件、想读懂TF-A代码或者正准备在新芯片上跑BL31的工程师参考。全文按照我的工程习惯组织启动流程、源码模块、关键安全机制都会给对应代码路径移植部分给了可直接照抄的思路。1. ATF为何存在ARM安全固件的信任根与分级体系1.1 安全世界和普通世界ATF的立足点先聊一个最基本的问题ATF解决的是什么事。ARM从Cortex-A系列开始引入TrustZone技术把整个系统划分成安全世界Secure World和普通世界Normal World。普通世界跑Linux、Android、RTOS安全世界跑可信执行环境TEE比如OP-TEE或Trusty。两者通过SMCSecure Monitor Call指令互相跳转而负责这个跳转、管理安全状态切换、提供电源管理接口的底层固件就是ATF。ATF运行在最高的异常级别EL3这话怎么理解ARMv8的异常级别从低到高是EL0、EL1、EL2、EL3。Linux内核跑在EL1虚拟化场景下Hypvisor跑在EL2而EL3就是整个系统里权限最大的那一层。打个比方EL3相当于物业总配电房的钥匙Linux和TEE再怎么折腾都只是在自己那间屋子里动电闸真正能拉总闸、换总路的人只有EL3的ATF。所以你会发现ATF不是一个传统意义上的“裸机程序库”它是一个安全管理器。它管安全启动、管异常级别切换、管PSCI电源管理、管SDEI事件还管内存隔离配置。没有ATFARMv8平台上Linux的CPU启动、休眠、重启这些操作就没有一个可依赖的底层实现路径。1.2 BL1/BL2/BL31/BL32/BL33一条链路看启动全貌ATF源码和运行时被拆成好几个组成部分最常出现的就是BL1、BL2、BL31、BL32、BL33这一串缩写。很多新人一开始就被这五个名字绕晕其实记住一个原则就不乱BLBoot Loader后面的数字越大阶段越靠后代码在系统里存活的时间也越短除了常驻的BL31。我在源码里对应关系整理成一张表方便对照理解阶段运行位置职责生命周期典型实现BL1SRAM或BootROM加载最小初始化、信任根验证、加载BL2启动早期执行完跳转ATF自带bl1BL2SRAM/DDR可信启动引导加载BL31/BL32/BL33并验签加载完BL31后销毁ATF自带bl2BL31DDR常驻EL3运行时固件提供PSCI/SDEI等运行时服务系统运行期间常驻ATF自带bl31BL32安全世界DDRTEE操作系统系统运行期间常驻OP-TEE等BL33普通世界DDR正常世界引导程序引导操作系统后转交U-Boot、EDK2BL1作为信任链的第一环代码通常由SoC内部的BootROM加载到片内SRAM然后BL1校验BL2。BL2主要负责从存储介质里找到BL31、BL32、BL33验证签名后把它们放到内存正确的位置。BL31启动后不会退出它会一直驻留在EL3等普通世界通过SMC指令向它请求服务。后面讲源码结构时你会发现BL1、BL2的代码相对少而且路径集中真正复杂的是BL31因为它要同时处理运行时服务、安全调度和上下文切换。1.3 ATF不是唯一答案但已是事实标准早年间ARM平台上也有其他方案比如U-Boot早期提供的PSCI支持以及各芯片厂商自己闭源的Secure Monitor。但ATF开源、免费、有ARM官方持续维护加上QEMU和FVP模拟器原生支持几乎所有主流ARM SoC和开发板最终都回归到了TF-A这条路。对新平台而言如果不是想自己从零实现EL3固件ATF基本就是唯一值得考虑的基础。我之前做NXP i.MX8M系列板卡时官方BSP就是把ATF、OP-TEE、U-Boot三者打包在一起ATF负责最底层那层安全链路。这种分工合作的方式已经成了当前ARM生态里的默认组合。2. 源码结构全景拿到TF-A仓库后怎么下口2.1 顶层目录逐个过一遍ATF源码目前在GitHub上的arm-trusted-firmware仓库维护官方名称已经改为Trusted Firmware-ATF-A仓库里仍然保留了ATF的历史命名习惯。拿到代码后先别急着改代码把顶层目录过一遍后面查问题会快很多。关键的目录有这些bl1BL1阶段的源码主要是boot loader第一阶段实现包含aarch64下的启动汇编。bl2BL2阶段的源码负责可信启动加载流程。bl31BL31运行时固件源码包括入口、主流程、以及aarch64下的异常处理。common跨阶段公共代码比如启动流程的公共宏、参数传递、镜像加载逻辑。plat平台相关代码所有板级适配都放这里也是移植工作的主战场。drivers各类驱动包括GIC中断控制器、UART串口、存储等底层驱动。lib公共库比如el3_runtime、psci、xlat_tables_v2、el3_common等。servicesEL3运行时服务包括PSCI、SDEI、SPMD、std_svc等。include公共头文件按目录分好类比如include/lib、include/plat、include/services。tools工具链包括fiptool、cert_create、sptool等。docs官方文档平台移植前强烈建议先翻这里的porting-guide。我最早接触ATF时一上来就钻进了bl31的代码里结果被各种宏定义绕晕。后来才发现正确的下口顺序应该是先看docs目录下的porting-guide.rst再看platform porting flow然后顺着BL阶段去追代码。这个顺序能少走很多弯路。2.2 核心服务模块PSCI、SDEI、SPM这些服务都在哪ATF在EL3层提供的服务在源码里对应services目录下的各个子模块其中PSCI是我认为最核心的。PSCIPower State Coordination Interface负责CPU的启动、关闭、挂起、系统重启和关闭是Linux和ATF之间电源管理的主要协议。它在services/std_svc/psci/目录下实现包括psci_main.c、psci_cpu_on.c、psci_common.c这些文件。举例来说Linux内核要启动一个CPU核心时会通过SMC指令调用PSCI_CPU_ONATF在EL3收到这个请求后把目标CPU从OFF状态带起来设置好入口地址然后让它跳到内核指定的启动入口。整个过程涉及电源域状态管理、当前核心和目标核心的上下文切换不是简单的“写个寄存器就能跑”。SDEISoftware Delegated Exception Interface则用于软中断事件的分发在services/std_svc/sdei/目录下。它允许普通世界通过SMC注册事件处理函数ATF在收到事件后帮普通世界切换上下文并跳转到处理函数。SPMD和SPM则负责与管理安全分区相关是Arm CCA等新特性的基础。2.3 构建系统与镜像生产流程ATF使用make作为构建系统构建配置的核心是PLAT变量指定要编译的平台比如make PLATfvp或者make PLATqemu。构建完成后产物会输出到build/平台/debug|release/目录下常见的产物有bl1.bin、bl2.bin、bl31.bin。这些镜像最终需要被打包成FIPFirmware Image Package使用tools/fiptool工具完成。打包命令大致长这样fiptool create \ --tb-fw build/qemu/release/bl2.bin \ --soc-fw build/qemu/release/bl31.bin \ --nt-fw u-boot.bin \ --tos-fw tee.bin \ fip.bin值得注意的是BL1通常不打包进FIP因为BL1要放在BootROM加载的位置而BL2开始往后的所有镜像都可以由BL1/BL2从FIP里解析加载。fiptool不仅能制作FIP还能列出FIP里的镜像列表。3. 源码级安全审计信任根、SMC分发与内存隔离3.1 TBB信任链与证书链ATF的安全启动机制叫Trusted Board BootTBB核心思路是建立一条从SoC信任根到最终启动镜像的验证链。这就像进一栋大楼你必须先验证第一道门的钥匙信任根之后每一道门都由上一道门的验证结果来背书。源码里TBB相关代码在drivers/auth/目录下包括认证框架auth_mod.c、镜像解析器img_parser_mod.c、证书解析器cert_parser等。在TBB模型里BL1内置了Root Of Trust Public KeyROTPK也就是根公钥。启动时BL1拿ROTPK验证BL2的证书和镜像BL2再用从证书链里提取的密钥去验证BL31、BL32、BL33这些后续镜像。实际开启TBB需要在编译时打开开关同时还要指定mbedtls库路径make PLATqemu TRUSTED_BOARD_BOOT1 \ MBEDTLS_DIR/path/to/mbedtls \ GENERATE_COT1 \ all如果用的是签名证书链还要用cert_create工具生成密钥和证书。我的建议是第一次跑通平台时先不要开TBB等裸启动链稳定了再做安全启动不然会出现“前面还没跑亮后面验签又失败”的叠加问题。3.2 EL3入口与SMC调用路径ATF里最值得读的一段启动代码是BL31的入口。BL31的启动流程从bl31/aarch64/bl31_entrypoint.S开始经过el3_entrypoint_common宏完成异常向量表设置、系统寄存器初始化和栈指针配置然后调用bl31_main。bl31主流程里最关键的几步是bl31_early_platform_setup2平台早期初始化比如配置串口、读取内存信息。bl31_plat_arch_setup配置MMU和内存映射。bl31_platform_setup平台外设初始化。runtime_svc_init注册并初始化所有运行时服务。服务注册完成后BL31进入一个无限等待状态每次普通世界或安全世界发生SMC异常都会先进入bl31/aarch64/runtime_exceptions.S里的异常入口按照ESR_EL3的ECException Class字段判断异常类型。EC等于0x17表示这是SMC调用随后代码进入SMC处理路径解析FIDFunction ID。每个SMC服务在初始化时都通过DECLARE_RT_SVC声明一个rt_svc_desc结构体注册自己的OENOwning Entity Number、调用号范围、初始化函数和handler函数。运行时SMC分发逻辑根据FID里的OEN找到对应服务把控制权交给该服务的handler。看懂这条路径以后加自定义SMC服务就是顺水推舟的事情。3.3 内存隔离与MMU配置安全固件最怕的不是功能跑不通而是内存权限配置错误导致的安全漏洞。ATF通过MMU和TrustZone控制器把普通世界和安全世界的内存严格隔离开。在BL31阶段平台代码要用xlat_tables_v2库配置页表把内存区域划分为不同属性普通内存使用MT_MEMORY属性比如BL31的数据段、堆栈。设备内存使用MT_DEVICE属性比如GIC寄存器、UART寄存器。代码段MT_RO只读可执行。数据段MT_RW可读写但不可执行。实际配置时一般通过mmap_add_region或mmap_add把这些区域加到页表里然后初始化translation table并enable MMU。我见过太多因为漏配UART地址范围导致打印函数触发同步异常直接死机的案例。像GIC这类外设寄存器如果不标记为MT_DEVICE而误标成MT_MEMORY在ARMv8上会因为内存序问题导致中断配置错乱轻则功能异常重则卡死。所以platform移植时第一件事就是把这几个关键区域的属性核对清楚。4. 平台移植落地从零部署一个新平台4.1 最小编译目录与platform.mkATF平台移植的第一步是在plat目录下创建自己的平台目录通常路径是plat/厂商/板卡名。目录里必须有platform.mk、平台头文件和平台C文件。platform.mk是整个移植的入口它告诉构建系统这个平台需要编译哪些源文件、使用哪些编译选项、内存布局如何定义。我习惯拿一个同类平台做底子来改。比如想做一个和QEMU virt类似的虚拟平台就先看plat/qemu/platform.mk里写了什么。一个最小的platform.mk大致是这样的include plat/common/plat_common.mk # 定义BL31源文件 BL31_SOURCES \ plat/demo/demo_board/demo_board.c \ plat/demo/demo_board/demo_board_common.c # 平台内存布局宏 $(eval $(call add_define,PLAT_DEMO_BL31_BASE)) $(eval $(call add_define,PLAT_DEMO_BL31_LIMIT))platform.mk里还可以通过BL32、BL33变量指定TEE和正常世界引导程序的路径用CRASH_CONSOLE指定崩溃时用的串口用LOG_LEVEL指定日志级别。这些选项看着琐碎但每一个都直接影响镜像能不能在目标板上跑起来。4.2 内存布局BL31_BASE到BL31_LIMIT的取舍内存布局是移植过程中最需要谨慎的部分。BL31要放在内存的什么位置BL32放哪BL33放哪要用多大空间这些都要在平台头文件和链接脚本里明确。ATF对不同平台提供了一套自定义内存布局的宏比如BL31_BASE、BL31_LIMIT、BL32_BASE、BL32_LIMIT。我通常按照“BL31放低地址、BL32放BL31之后、BL33放更高地址”的原则设计。比如一个8GB内存的板子可以留出最前面的256MB给安全固件使用其中BL31位于0x04200000大小2MBBL32位于0x04400000大小8MB。链接脚本会按照这些宏生成bl31.elf的内存布局如果BL31_LIMIT设小了链接阶段就会发现溢出。这部分特别容易出现“能编译但一跑就崩”的情况尤其是BL31的栈和数据放在哪个区域在不同平台上差异很大。建议一开始内存区域宁可大一些并且加上越界检查等稳定后再收缩空间。4.3 必现平台函数清单BL31移植时必须实现一组平台函数否则构建会报未定义错误。我把BL31阶段最常见的那几个列出来刚做移植时直接对着这个清单逐项补齐函数阶段作用遗漏后果bl31_early_platform_setup2BL31早期串口、内存基本信息初始化打印不了日志无法调试bl31_plat_arch_setupBL31架构配置MMU、页表、内存映射一进C代码就同步异常bl31_platform_setupBL31平台外设、GIC、系统计数器等初始化中断和PSCI功能异常plat_get_syscnt_freq2运行时提供系统计数器频率PSCI延时和时钟相关功能错乱plat_crash_console_init崩溃时准备崩溃打印串口PANIC时完全黑屏plat_my_core_pos运行时返回当前CPU的逻辑ID多核启动错乱platform_get_core_pos运行时把物理核心映射到逻辑核心多核调度和中断路由异常这些函数里plat_crash_console_init最容易被忽略但在真机调试时几乎离不开它。没有它的话BL31一旦发生未捕获异常系统就是一片死寂只能靠JTAG或者示波器猜问题。有了它至少能看到原始的寄存器状态和调用地址。4.4 编译、FIP制作与烧写调试以QEMU平台为例完整编译一条链路可以用这个流程make CROSS_COMPILEaarch64-none-elf- PLATqemu DEBUG1 bl31如果要编译并打包BL1、BL2、U-Boot等还需要指定BL33路径然后用fiptool制作FIP再将FIP加载到目标存储介质。在QEMU virt平台上可以直接把FIP作为Flash加载启动后串口会输出BL1、BL2、BL31的日志。日志能打到BL31说明最先的启动链路已经通了接下来就可以开始调试BL33引导和TEE。如果是真机烧写方式取决于SoC的启动设备可能是SD卡、eMMC、NAND或USB下载。在拿到板子的前半天我建议先用厂商BSP里验证过的ATF构建产物做一次完整的启动确认硬件环境没问题再换上自己改的代码迭代。4.5 几项高频功能移植点除了把BL31跑起来实际项目里还经常要补这几块GICv3中断控制器初始化BL31平台启动时要初始化GIC包括配置GICD、GICC寄存器设置中断组和安全属性。系统计数器PSCI功能依赖系统计数器必须提供正确的频率和初始化逻辑。串口驱动BL31的日志输出全靠它移植时建议用最简轮询模式先不要用中断。唤醒源配置实现PSCI_CPU_SUSPEND和PSCI_SYSTEM_SUSPEND时需要结合具体硬件确认唤醒源和支持的电源状态。其他像RAS、AMU、MPAM这些可选特性功能模块在ATF里都有现成实现但每加一个特性都可能引入平台相关配置。我把这些特性当“后期加分项”绝不因为它们影响基础启动链路。5. 常见问题与排查实录ATF跑出问题的现场解决流程5.1 三个典型故障场景我把自己在真机和模拟器上踩过的坑整理成三个场景基本都是移植初期高频出现的。第一个是“BL31一进去就PANICPC停在某个未知地址”。这通常是MMU和内存映射配置问题比如代码段映射成不可执行或者把设备寄存器映射成了普通内存。定位方法很简单打开LOG_LEVEL查看PANIC时打印的ESR和FAR寄存器值再配合bl31.elf的符号表用addr2line就能看到撞在哪个函数上。第二个是“串口没有任何输出”。这种情况先确认是不是bl31_early_platform_setup2里串口初始化就没做对再看串口地址在MMU table里有没有映射。如果这两项都正常那就检查是不是BL31镜像根本没被加载到正确地址。别忘了CONSOLE的时钟是不是和实际硬件一致很多板子串口波特率对不上就是时钟配置错误。第三个是“PSCI调用返回错误码”。Linux执行CPU hotplug时报PSCI返回EINVAL之类常见原因是ATF里的PSCI ops没有实现对应函数或者平台电源域描述和实际硬件不匹配。可以打开内核PSCI的trace对照ATF的psci日志一起看。我用下面的表格做了一下速查现场排查时可以快速对照现象可能原因线索解决方向BL31进不去完全无输出镜像加载地址错误检查FIP中BL31入口核对内存布局和FIP打包参数同步异常PANICMMU映射错误查看ESR/FAR寄存器检查mmap_add_region属性串口乱码/无输出UART时钟或引脚配置错检查板级clock核对串口驱动中的时钟频率PSCI调用失败电源域描述不匹配查看PSCI返回错误码检查plat_setup里的power domain5.2 日志调试开关与模拟器技巧ATF的日志分级在include/common/debug.h里定义LOG_LEVEL从0到50分五档0代表关闭50代表最详细的VERBOSE。日常调试建议先拉到40INFO能看到BL31初始化时各个服务的注册情况。如果还不够细就拉到50但同时要注意日志太细会影响时序特别是涉及到PSCI电源切换时输出过多可能掩盖真实问题。在QEMU上可以用-gdb tcp::1234调试再用GDB的target remote连接。真机上如果没有JTAG我一般靠crash console打印。还有一个技巧在BL31的panic路径里ATF会把当时的ELR、FAR和ESR都打到崩溃串口上把这些信息记录下来再用objdump反汇编bl31.elf基本能定位到具体指令。5.3 性能与体积优化技巧BL31的镜像体积在一些存储敏感的场景下很关键。我的经验是先把LOG_LEVEL降到20NOTICE只保留必要提示然后关闭DEBUG模式去掉调试符号和断言最后通过裁剪未使用的服务和驱动来减体积。ATF通过每个服务的Makefile源文件列表控制编译内容没用到的服务直接不加入BL31_SOURCES即可。如果追求极致性能可以考虑ATF的链接时优化LTO支持在编译选项里加ENABLE_LTO1。这会延长编译时间换来一定体积和性能收益。但是LTO对工具链版本有要求升级编译器的同时一定要回归测试一遍启动链路。5.4 移植避坑建议最后聊几条移植层面的经验。第一不要在一开始就打开TBB安全启动先把裸启动跑通再逐步叠加安全特性。第二优先参考上游仓库里和自己平台最接近的现有实现比如同样是Cortex-A53的板子参考qemu或juno的代码比自己硬写GIC初始化要快得多。第三遇到版本差异时以官方docs文件夹里的porting-guide为准不要盲目相信老版本的博客和笔记。第四每改一次内存布局或者MMU映射都顺手更新平台头文件里的宏定义防止改着改着前后不一致这是我踩过最深的一次坑。移植过程的节奏感也很重要我一般遵循一条原则先让EL3的console响起来再谈其他。只要BL31能在目标板上打印出第一行日志这条链路就已经通了百分之八十后续无非是补齐功能和排查细节。这听起来简单但真正做起来需要把架构、源码和调试工具融会贯通。希望这篇基于源码的ATF评测和落地指南能让你在新平台的安全固件移植上少一点手足无措。
返回列表