ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

ARM可信固件ATF深度解析:从BL31架构到平台移植实战 ARM生态里Trusted Firmware一直是个让人又爱又恨的东西。爱的是它把ARMv8架构的安全启动、运行时代码提权、PSCI电源管理这些底裤级别的逻辑全部开源了恨的是它的代码结构复杂、抽象层极多新手第一次clone下来面对几十个目录和一堆平台宏定义很容易直接劝退。我前后在三个不同芯片平台的BSP工程里折腾过ATF从最开始的照葫芦画瓢改编译脚本到后来为了排查一个安全中断路由问题硬着头皮把BL31的runtime异常处理链路逐行读了一遍才算真正摸清了这套固件的脾性。这篇文章不打算做成文档翻译而是想站在一个“需要把ATF落地到自家板子”的工程师视角把ATF的架构全景、关键源码路径、安全审计要点和平台移植的具体步骤串一遍。如果你正准备在新项目里引入ATF或者正在为手里的开发板适配标准启动流程这篇文章应该能帮你省下不少翻源码和查邮件列表的时间。1. 内容整体设计与思路拆解1.1 ATF在ARM系统启动链路上的角色定位很多人把ATF理解成“一个引导程序”这是最大的误解。U-Boot、UEFI这类东西才是引导程序ATF的定位严格来说是“安全运行时固件”它活在系统启动的最早期负责建立安全世界Secure World和非安全世界Normal World之间的隔离边界。说得直白一点ARMv8架构的CPU从上电开始EL3Exception Level 3是最高特权级ATF的BL31就常驻在EL3它手里握着系统里最后的安全底线。整个启动链路通常是这样的BootROM加载BL1BL1加载BL2BL2加载BL31和BL33也就是U-Boot或UEFI。BL31在EL3初始化完成后会陷入一个等待状态把执行权交给BL33。之后系统里所有需要提权、需要操作安全资源的请求都得通过SMCSecure Monitor Call指令重新回到EL3由BL31统一受理和分发。这里有个特别容易混淆的点BL1和BL2只是“一次性使用”的启动阶段跑完就退场BL31才是那个从头站到尾的角色。所以ATF的绝大部分核心代码包括运行时服务Runtime Services、中断管理GIC/Interrupt Management、电源管理PSCI全都集中在BL31的源码目录里。搞清楚这个关系再去看代码就不会晕头转向了。1.2 源码树目录结构与编译框架的底层逻辑ATF的源码树第一眼看去很劝退但其实设计得相当规矩。顶层目录不多每个都有明确归属bl1/、bl2/、bl31/是三大启动阶段的子目录plat/存放所有平台相关代码drivers/是各种外设驱动lib/是通用库include/是头文件。这种划分本质上是为了应对“各种芯片百花齐放”的现实ARM只提供架构标准具体到GIC版本、UART型号、内存分布每家芯片厂商都不一样所以平台代码必须从核心逻辑里抽离出来。编译系统用的是makefile体系入口是顶层Makefile通过PLAT变量指定平台目录。比如make PLATversal就会去plat/xilinx/versal/下找平台描述文件。平台目录里最核心的文件是platform.mk它通过一系列变量告诉构建系统“我这个平台需要编哪些源文件、用哪些编译选项、内存布局是什么”。这种设计的精妙之处在于你移植一个新平台时完全可以只写plat/下的内容核心的BL31逻辑一行都不用动。但这也意味着你必须严格遵守平台接口的约定——plat_get_next_bl_params、bl31_plat_get_next_image_info这些接口的名字和参数都是定死的填错一个链接阶段可能没事跑起来立刻翻车。1.3 为什么安全固件必须用“纵深防御”思路来审计ATF是安全固件不是普通功能固件所以看它的代码和看U-Boot代码的心态要完全不一样。普通固件最关心“功能是否正常”安全固件最关心“异常输入是否会导致提权或逃逸”。ARM官网的文档把ATF的安全目标写得很清楚保护安全世界的数据、保证非安全世界无法直接访问EL3资源、确保安全中断能及时响应。基于这个目标我在审计ATF源码时基本会从三个维度入手。第一是内存隔离检查BL31是否把所有关键数据结构放到了安全内存区域trusted_ram的物理地址和大小配置是否合理。第二是入口校验检查SMC处理函数是否对参数做了边界检查特别是smc_arg里的地址是否被执行前验证过。第三是中断路由检查安全中断是否被正确配置到EL3非安全中断能否被正确转发回Non-Secure世界避免中断逃逸。这三个维度踩实了整个固件的地基才算稳固。2. 核心细节解析与实操要点2.1 BL31的启动流程与上下文保存恢复机制BL31的代码入口在bl31/bl31_main.c里的bl31_main()函数。这个函数的工作可以分成四步先调用bl31_early_platform_setup2做早期平台初始化比如设置UART、读取内存布局然后调用bl31_plat_arch_setup建立MMU页表和内存映射接着是bl31_platform_setup做普通平台初始化比如配置GIC、初始化电源管理控制器最后调用bl31_lib_init完成各个运行时服务Runtime Services的注册。这里有一个关键细节是上下文保存恢复机制体现在bl31_entrypoint.S。ATF在EL3接收来自Non-Secure世界的SMC请求后第一步做的事不是去执行请求而是把当前CPU的通用寄存器、系统寄存器完整保存到cpu_context结构体里然后切换到context_mgmt层去分发请求。这个机制极其重要因为EL3和Non-Secure世界共享同一套物理寄存器如果不做保存和恢复安全服务和普通世界就会互相踩踏。从实操角度看context_mgmt库里最值得细读的函数是cm_prepare_el3_exit。它在每次返回到Non-Secure世界之前会把之前保存的上下文重新装载到寄存器并把SPSR_EL3、ELR_EL3设置成目标状态要求的特权级和跳转地址。这里如果配置错了系统会出现“返回后PC直接跑飞”的诡异现象而且是偶发的、极难复现的。2.2 SMC分发机制与PSCI电源管理实现的协同SMC分发是BL31的一个核心枢纽。所有从Non-Secure世界进来的SMC请求首先经过smc_handler64汇编入口然后进入runtime_svc.c框架。ARM定义了标准的服务ID格式比特32到35是服务类型比如ARM_SMC_TYPE里的Fast Call和Yielding Call比特24到31是服务商ID比如ARM的标准服务商是0比特8到15是服务函数编号。这套格式的用意是让不同服务商、不同服务类型的请求可以互不干扰地共享同一个入口。在所有这些SMC服务里PSCI电源管理接口是最常用也是最容易出问题的。PSCI实现分布在services/std_svc/psci/目录下核心逻辑是psci_cpu_on、psci_cpu_off、psci_suspend这几个函数。以psci_cpu_on为例它要做的事情包括校验目标CPU的ID合法性、验证入口地址是不是Non-Secure世界的合法地址、根据affinity level找到对应的电源域、通过psci_plat_pm_ops里实现的操作函数去真正操作电源控制器。这里我要特别提醒移植新手psci_plat_pm_ops里的pwr_domain_on操作是平台相关的芯片厂商的TRMTechnical Reference Manual里通常会写清楚电源控制器的寄存器序列。很多人图省事直接复制参考平台的实现结果在目标平台上CPU根本拉不起来或者拉起来后跑飞。这个问题我踩过一回最后定位下来是漏写了一个PWR_ON的确认轮询。2.3 安全启动链路从BL1到BL31的逐级验证安全启动Trusted Boot是整个ATF安身立命的根本。ARM的Trusted Board BootTBB方案里每一级固件在加载下一级之前都要验证下一级的镜像签名和哈希。具体来说BL1验证BL2BL2验证BL31和BL33采用的方式是证书链芯片出厂时烧入ROT密钥Root of TrustROT密钥签发BL1的证书BL1证书再签发BL2证书以此类推。drivers/auth/目录下是认证框架的实现。它把认证流程抽象成了四步先通过auth_mod_get_data获取待验证的数据然后调用img_parser_mod解析出证书和签名字段接着用crypto_mod做哈希和验签运算最后通过auth_mod_verify_signature把结果和信任根的公钥做比对。ARM提供了mbedTLS作为默认的软件加密实现路径在drivers/auth/mbedtls/你只需要在platform.mk里配置TRUSTED_BOARD_BOOT1和GENERATE_COT1编译系统就会自动生成签名工具和证书链。这个链路在工程实现上有两个坑。第一个坑是BL2的大小限制很多芯片的SRAM容量有限开启TBB后BL2要额外容纳证书解析和验签的代码编译时很容易爆掉得靠PLAT_XLAT_TABLES_DYNAMIC和裁剪mbedtls配置来节省空间。第二个坑是密钥管理开发阶段用development密钥就行但量产时必须换成你自己的密钥并且把私钥放进HSM里保护。有人在开发板上用默认密钥跑了几个月量产时发现密钥结构不兼容需要重新设计证书格式这个返工成本是极高的。3. 实操过程与核心环节实现3.1 为一个新平台编写最小化平台描述文件我自己移植ATF的经验是从一个最小化的平台目录开始的。假设要在plat/mycompany/fpga1/下创建一个新平台最基础的文件有四个platform.mk、platform_def.h、plat_setup.c、plat_pm.c。platform.mk的写法是这样的先把变量列出来告诉构建系统平台名和需要包含的源文件PLAT : mycompany-fpga1 # 指定平台源文件 PLAT_BL_COMMON_SOURCES plat/mycompany/fpga1/plat_setup.c BL31_SOURCES plat/mycompany/fpga1/plat_pm.c # 指定内存布局变量 ARM_GIC_BASE : 0x2F000000 ARM_UART_BASE : 0x1C090000platform_def.h里要定义好几个关键宏PLAT_PHY_ADDR_SPACE_SIZE定义物理地址空间大小PLAT_VIRT_ADDR_SPACE_SIZE定义虚拟地址空间大小PLAT_MAX_PWR_LVL定义最大电源层级还有PLATFORM_CORE_COUNT核数。这些宏直接决定BL31的MMU页表有多大、PSCI的电源管理树有多深设小了链接报错设大了浪费内存。plat_setup.c里至少要实现bl31_early_platform_setup2和bl31_platform_setup。前者的核心任务是读取meminfo结构体告诉BL31哪些内存区域是安全的、哪些是Non-Secure的。后者的任务通常是初始化GIC和系统控制器。写的时候要留意bl31_early_platform_setup2运行的时候MMU还没开不能直接解引用复杂指针只能做寄存器操作和简单的内存读写。3.2 平台内存映射与MMU初始化参数配置MMU初始化是ATF移植中最容易出问题的地方之一。ATF在BL31阶段会建立自己的页表用来映射两类内存一类是BL31代码所在的BL31_BASE加BL31_SIZE区域另一类是外设寄存器的映射区域比如GIC、UART的寄存器地址。在plat_setup.c里你通常要重写plat_get_next_bl_params和plat_setup_page_tables这两个函数。前者负责告诉BL31“下一个镜像BL33的内存参数是什么”后者负责把地址映射填进页表结构。我的经验是外设映射区域的大小要稍微留一点余量但也不能滥用MT_DEVICE类型映射大块物理地址否则会产生覆盖其他外设的风险。举个例子假设GIC基地址是0x2F000000映射大小是0x10000代码可能是这样的static const struct mmap_region plat_mmap[] { /* GIC映射 */ { ARM_GIC_BASE, ARM_GIC_BASE, 0x10000, MT_DEVICE | MT_RW | MT_SECURE }, /* UART映射 */ { PLAT_UART_BASE, PLAT_UART_BASE, 0x1000, MT_DEVICE | MT_RW | MT_SECURE }, { 0 } };写好之后在plat_setup_page_tables里调用mmap_add(plat_mmap)然后通过enable_mmu_el3开启MMU。调试的时候如果系统在进入BL31后立刻异常十有八九是页表里漏映射了某个外设地址导致访问时触发DFCData Fault这个问题可以通过打开DEBUG1和LOG_LEVELLOG_LEVEL_VERBOSE来定位。3.3 电源管理操作函数集的具体实现路径PSCI的电源管理操作是平台移植的核心难点。需要实现一个plat_psci_ops结构体里面主要挂四个操作pwr_domain_on、pwr_domain_off、pwr_domain_suspend、pwr_domain_resume。这些操作会通过psci_set_plat_ops注册到框架里之后PSCI框架就通过这个接口来操作具体的硬件。以pwr_domain_on为例它的实现基本是这两步先通过psci_get_aff_info校验CPU的亲和性信息然后写电源管理控制器的寄存器把目标CPU从掉电状态唤醒。飞腾平台和部分ARM SoC的电源控制器寄存器布局不一样建议对照芯片TRM的“Power Control Register”章节来写。关键点是启动后要轮询确认CPU真的上电成功否则后续psci_node_hw_state的状态不对调度器会看到一堆“CPU offline”的假象。suspend和resume的实现更麻烦一点。挂起时要把CPU核的上下文保存到安全内存里包括通用寄存器、系统寄存器特别是sctlr、tcr、ttbr0_el1、vbar_el1这些。唤醒时要从保存的上下文恢复执行。ATF提供了一个宏el3_exit来做最后的恢复和返回但恢复前必须保证mmu_init时映射的内存区域还在否则恢复完寄存器之后一跳转又fault了。3.4 基于RK3328平台的完整移植实录为了更直观地说明移植过程我拿RK3328作为例子简述一下把ATF跑起来的完整路径。RK3328是四核Cortex-A53GIC-400支持DDR3/DDR4/LPDDR3是块非常适合练手的板子。第一步是创建平台目录把plat/rockchip/rk3328/作为起点。这个目录里已经有官方参考实现所以“移植”更多是理解加剪裁。第二步是查看platform.mk里的编译选项确认RK3328_SECURE_DEBUG_ENABLE、RK3328_ATF_LOAD_ADDR这些宏的值跟你的DDR初始化代码是否匹配。第三步是修改platform_def.h里的PLAT_RK3328_UART_BASE如果你的调试串口硬件接在UART2上这里就要设成UART2的地址否则BL31的Log会打到空气里。接着编译命令大概长这样make PLATrk3328 DEBUG1 LOG_LEVEL40 bl31编译产物在build/rk3328/debug/bl31.elf和bl31.bin。调试阶段最重要的是能通过UART看到BL31的启动日志。如果卡在某一步不动优先检查两个点一是DDR初始化是否成功二是你的TrustZone地址空间配置是否把BL31的加载地址划给了Secure世界。很多RK3328的板子直接用Rockchip的miniloader来加载ATFATF加载地址必须跟miniloader的配置一致否则一执行必挂。最后一步把bl31.bin和U-Boot的u-boot.itb打包进一个启动镜像里。RK平台的打包脚本会生成trust.img和uboot.img烧录到SD卡或eMMC的对应分区。跑起来之后输入smc相关的测试命令去验证PSCI功能是否正常比如cpu hotplug测试# 在U-Boot中测试CPU off/on md 0xff000000 # 在Linux中测试CPU热插拔 echo 0 /sys/devices/system/cpu/cpu3/online echo 1 /sys/devices/system/cpu/cpu3/online如果cpu3能顺利下线再上线说明ATF的PSCI移植基本过关。如果上不了线去查plat_pm.c里的rk3328_pwr_domain_on实现重点看CPU的启动地址和电源域的assert时序。4. 常见问题与排查技巧实录4.1 镜像启动阶段卡死且无日志输出这是移植ATF时第一大难题。代码编译过了烧录进去系统毫无反应。首先排除一个最常见的低级错误烧录地址对不上。你用bl31.bin烧录时得确认加载地址跟platform_def.h里的BL31_BASE一致。如果你的加载地址是0x100000但BL31_BASE写的是0x40000000系统一上电就跑到错误地址去了。第二个排查重点是UART初始化。ATF的日志输出依赖console_core_init和console_core_putc。如果你的UART时钟频率没有在platform_def.h里配置准确打印出来的可能是乱码或者根本无输出。我建议在bl31_early_platform_setup2的最前面加一个简单的GPIO翻转调试点先确认BL31的入口代码是否已经执行再排查UART问题。第三个可能原因是TrustZone配置。有些平台的SCCSystem Control Coprocessor寄存器控制了安全属性的默认值如果你的BL31内存区域被设定成Non-Secure那么EL3代码去取指时会直接触发权限错误这种错误往往没有日志。排查方法是烧录一个不带安全校验的最小BL31逐步打开特性直到定位到是哪一步引入了问题。4.2 SMC返回异常状态导致Linux内核panicLinux内核在启动时会调用一连串的PSCI接口来查询或设置CPU状态。如果你移植的ATF对某个PSCI版本号或者函数ID支持不全内核会panic报错典型如[ 5.123456] psci: failed to boot CPU3 (-22)这种问题多半出在PSCI版本协商上。Linux的PSCI驱动在初始化时会发一个PSCI_VERSION查询如果ATF返回的版本号内核不认识后续的CPU_ON就会被拒绝。解决方法是把ATF里的PSCI_VERSION宏改成内核支持的版本通常支持到1.1就够用。另一种情况是SMC函数ID的映射表出了问题。ATF的runtime_svc_descs结构体里定义了每个SMC服务对应的启动等级、调用类型、处理函数。如果你的平台在bl31_main阶段没有正确注册std_svc和arm_std_svc所有PSCI请求都会走默认处理分支返回NOT_SUPPORTED。查这个问题最快的办法是打开DEBUG1看ATF的日志里有没有Runtime service ... registered这几行标记。4.3 安全中断响应被延迟或丢失安全中断Secure Interrupt是ATF必须保障的关键能力。如果安全中断丢失那么你整个安全世界的功能比如可信执行环境TEE的调度都会跟着崩掉。在GICv2时代ARM推荐的安全中断处理方式是在BL31初始化时把GIC的GROUP0配置为Secure interruptsGROUP1配置为Non-Secure interrupts并在EL3的异常向量表里安装专门的处理入口。如果你发现安全中断响应延迟先确认GIC的优先级配置。ATF里有一个宏叫PLAT_ARM_GIC_CPU_IF它定义了CPU接口的基地址。优先级掩码寄存器PMR如果被设置成一个很低的阈值可能导致低优先级的安全中断发不出来。另一个隐蔽的问题是IAR寄存器读取时机如果你在异常入口处读取ICC_IAR1_EL1而不是ICC_IAR0_EL1就会读错中断号导致中断丢失。调试安全中断问题我习惯在GIC初始化完成后直接写一个测试SMC手动触发一个SGIsSoftware Generated Interrupt观察中断是否被EL3正确捕获。这个方法比依赖TEE的完整流程要快得多能快速判断是GIC配置的问题还是上层中断分发的问题。4.4 问题速查表官方边界之外的排错参考现象可能原因排查建议BL31无日志输出UART配置错误/加载地址错误检查UART时钟和基地址用GPIO调试点确认入口BL31启动后死循环MMU页表映射缺失或内存属性错打开LOG_LEVEL_VERBOSE检查页表映射日志Linux CPU热插拔失败PSCI版本不匹配/电源操作未完成检查PSCI_VERSION宏确认pwr_domain_on轮询逻辑安全中断不响应GIC优先级/中断分组配置错误用SGIs测试中断路径核对ICC_IAR寄存器读取系统随机重启TrustZone地址空间设置错误核查TZASC寄存器确认BL31内存被标记为Secure这张表是我个人排错经验的沉淀不一定覆盖所有平台的怪异行为但大方向是一致的。在碰到死活定位不了的问题时回到基础的ARM ARM手册和芯片TRM比在网络上翻帖子有效得多。5. 平台移植的工程化建议与落地心得5.1 平台代码的分层设计与复用策略ATF的plat/目录下官方实现已经沉淀了大量可复用的代码块。比如plat/common/里提供了一套通用的平台实现包括aarch64平台启动代码、GIC驱动框架、串口调试接口等。对于大多数新平台你真正需要从头写的其实只有三类代码平台内存布局配置、平台电源管理操作、平台GIC配置。这三类代码的硬件依赖最强芯片差异最大剩下的几乎都可以从官方参考平台里借过来改。我在实际工程里更推荐一种做法不要直接在plat/下新造轮子而是先选一个与你芯片架构最接近的官方平台目录复制一份然后逐步修改。比如你的芯片是Cortex-A55 GIC-600那就先参考plat/arm/board/fvp或者plat/mediatek的某个平台把GIC初始化改成GIC-600的寄存器序列把电源管理改成自家电源控制器的操作这样能极大降低起步成本。5.2 调试环境搭建JTAG、串口日志与Trace工具的组合调ATF跟调应用软件完全是两码事普通printf大法在BL31早期阶段并不可靠。我强烈建议在动手移植之前先把JTAG调试环境搭好。ARM DS-5、Lauterbach Trace32、OpenOCD都可以关键是能挂到CPU的EL3上能够在BL31入口处打断点查看寄存器和内存状态。串口日志是另一条腿。ATF的Log系统可以通过LOG_LEVEL宏控制复杂度在开发阶段把它拉到LOG_LEVEL_VERBOSE能看到每个阶段的详细打印。但注意有些打印本身有开销在时序敏感的场景比如PSCI suspend唤醒的路径上过度打日志会导致时序变化掩盖真正的问题。我一般调试时开Verbose定位差不多后降回LOG_LEVEL_INFO再验证一遍。Trace工具在三板斧里属于进阶选项适合排查那种偶发的、无法用断点捕获的问题。ARM CoreSight的ETM/PTM模块可以把CPU的指令流实时输出到Trace工具里回溯现场非常方便。不过Trace工具的配置成本高不是每个团队都有我的建议是没有Trace也能干但有了Trace能把你从“靠猜”的泥潭里拉出来。5.3 安全固件的发布构建与版本管理要点开发阶段和发布阶段的构建配置必须分开。开发阶段通常是DEBUG1、未启用TBB、日志全开这种镜像的用途是调试和验证功能。发布阶段则应该是DEBUG0、启用TRUSTED_BOARD_BOOT和GENERATE_COT并且替换掉默认的开发密钥。密钥管理这块我要多说一句。很多团队踩过同一个坑代码仓库里不小心把私钥提交进去了。ATF的密钥生成工具cert_create支持从文件读取私钥如果你把生成的private_key.pem当真提交到了Git仓库一旦仓库泄露整个信任链就废了。建议在.gitignore里明确排除所有*.pem文件并且考虑使用HSM或密钥管理服务来保存私钥发布构建机只在构建时按需拉取密钥。版本管理方面ATF的版本号结构是v2.x.y在Makefile里定义了VERSION_MAJOR和VERSION_MINOR。建议你在自己的平台目录里增加一个plat_version.c把自定义平台版本信息写进BL31的Log里这样现场出问题时能快速分辨是哪一版固件。这种习惯在量产设备上排障时能省下大量时间。6. 写在最后的工程思考和扩展方向ATF这套代码我前前后后读了不少也折腾了不少一个很深的体会是它的抽象设计本身就是为了对抗芯片厂商的碎片化所以学习ATF一定要先抓框架再抠细节。框架搞懂了具体平台差异全部落到几个关键文件里排查问题会高效得多。如果后续想继续深入有两个方向特别值得研究。一个是FF-AFirmware Framework for Arm A-profile标准它把EL3的运行时服务规范进一步统一化在支持FF-A的固件上TEE和Normal World之间通信会更加标准化。另一个是RASReliability, Availability and Serviceability扩展它让EL3能够捕获和处理硬件的错误事件是服务器级ARM平台必须考虑的可靠性能力。这两个方向都是在ATF现有架构上展开的打牢基础之后进入会非常顺畅。最后再分享一个小经验ATF的邮件列表和ARM官网的文档是最好的学习资料但我发现很多人忽略了一个更接地气的东西——各芯片厂商在GitHub上开源的ATF fork。比如Rockchip、NXP、Xilinx都有自己的版本里面针对自家芯片增加了大量平台代码和修复补丁。读这些fork的提交记录你往往能看到官方代码里面没写的硬件坑和适配细节这个价值是任何文档都替代不了的。
返回列表