
1. 内容整体设计与思路拆解1.1 一文说清ATF到底是什么很多从事嵌入式Linux或Android底层开发的工程师第一次接触ARM Trusted Firmware简称ATF现在官方常称TF-A时通常是一脸懵的。市面上讲U-Boot、讲Linux内核的资料一大把但系统讲ATF的资料却少得可怜。这导致不少人有个误解以为ATF是个类似U-Boot的引导程序把U-Boot加载起来就完事了。实际完全不是这样。ATF是一套运行在ARM处理器安全侧Secure World的固件集合它不仅仅负责上电引导更核心的职责是提供安全运行时服务、可信启动流程、以及在不同特权级和安全状态之间安全切换的机制。你在设备上看到的armv8/armv9架构、TrustZone、PSCI电源管理、安全启动Secure Boot、TEE可信执行环境这类词最终落地都绕不开ATF。为什么很多人看得懂U-Boot却看不懂ATF最根本的原因是视角不同。U-Boot解决的是“怎么把内核加载到内存并跳过去”的问题而ATF解决的是“在处理器刚上电、还处于最高特权级EL3时如何以安全可控的方式建立整个系统的执行环境”的问题。它站在系统启动的最顶端是安全信任链的根。这篇文章的来源是我对一个基于Rockchip RK3588平台的商业项目做ATF基线审计和移植时的完整复盘。从架构阅读、安全功能审计到平台适配和启动调试前前后后花了几周时间。我会把整个思路、关键代码路径、和踩过的坑全部梳理出来给准备接触ATF或者正在做平台移植的工程师一条相对完整的路线。1.2 ATF在整个启动链路中的位置要理解ATF首先得把它放进完整的启动链路里看。以经典的ARMv8-A平台为例典型启动顺序是芯片内部的BootROM固化在硅片里的程序执行根据拨码或烧录状态决定从eMMC、SD、USB等介质读取下一级镜像。读取到的第一份镜像就是ATF的BL1Boot Loader stage 1。BL1做基础硬件初始化时钟、DDR、串口等的最小集然后从可信存储位置加载BL2。BL2继续初始化加载BL31EL3 Runtime Firmware、可选的BL32TEE OS、以及BL33通常是U-Boot或UEFI。BL31常驻内存一直留在EL3向非安全侧的Linux内核提供PSCI等运行时服务。Linux内核在EL2虚拟化场景或EL1非虚拟化场景正常运行所有安全相关请求通过SMCSecure Monitor Call指令陷入EL3交给BL31处理。这个流程里最容易被忽略的一点是ATF不止在启动阶段干活它是一个“启动后依然活跃”的运行时组件。BL31常驻DDR里一个被保护的region类似你的电脑里有一个独立于操作系统之外的安全固件一直在跑——这么理解就贴切了。对于做系统移植的工程师来说你的任务时间线一般是这样芯片原厂给你一套完整可启动的ATF源码和烧录包——你验证它在你的板子上能跑——然后根据你的外设、内存颗粒、调试口差异做适配——最后把安全启动和TEE方案集成进去。看起来步骤不多但每一步都有很多暗坑。1.3 为什么需要专门做一次“工程审计”我在很多项目里见过一种危险做法直接把芯片原厂提供的ATF二进制丢进烧录工具能开机就认为万事大吉。短期看没毛病长期看隐患极大。因为ATF是整个系统安全信任链的地基它的错误不像应用层软件那样崩溃几次就能暴露往往是在特定攻击场景或者极端边界条件下才会触发。所以我建议凡是涉及安全功能的设备都应该把ATF源码做一次系统性的审计。审计的核心目标包括确认工程量级和组成部分知道谁在什么时候执行、访问哪些硬件资源。梳理信任链搞清楚每个镜像的加载和校验方式确认是否存在被绕过验证的可能。检查TOCTOUTime-of-check Time-of-use问题即“校验时是好的、运行时被换了”的经典攻击面。验证BL31提供的SMC接口是否严格做了调用来源和参数校验这部分是攻击者最容易接触到的接口。评估平台配置的合理性比如BL31运行基址、内存权限配置、PSCI功能是否完整。这听起来像安全团队的活但实际做平台移植的工程师也同样需要掌握。因为你不知道什么时候就要自己改一处平台代码不懂审计原则的人很容易随手引入一个安全隐患。2. ARM Trusted Firmware源码架构全景拆解2.1 源码目录结构与各目录职责拿到TF-A源码后首先要做的是把目录结构看懂。TF-A的代码仓库地址是https://github.com/ARM-software/arm-trusted-firmware后来被迁移到了TrustedFirmware.org托管官方的参考实现是主线master分支。整个仓库的顶层目录结构大致如下bl1/BL1阶段源码负责最基础的启动。bl2/BL2阶段源码负责加载和认证后续镜像。bl31/BL31运行时固件源码包含SMC分发、PSCI实现、运行时服务框架。bl32/BL32TEE OS的启动集成代码实际TEE OS本体如OP-TEE是独立仓库。plat/平台相关代码这是你做移植时最常动的地方按厂商分目录存放。drivers/各类驱动包括GIC、UART、DDR、IO存储器等。include/通用头文件包括架构定义、平台接口定义、常量定义等。lib/通用库如标准C库子集、跳转表、内存操作等。common/通用代码如启动流程的基本框架、Blob验证逻辑等。services/各类运行时服务包括标准SMC服务、PSCI服务、SPDSecure Payload Dispatcher等。tools/各种工具包括证书生成工具cert_create、FIP打包工具fiptool等。fdts/设备树源文件ATF也使用设备树描述平台硬件信息。初次接触的人容易犯一个错误钻进bl1的汇编代码里出不来。其实你不必逐行抠汇编。ATF强调可移植性通用流程代码用C写的部分很多只有极小部分例如CPU reset、异常向量表、上下文切换才用汇编。你真正需要精读汇编的地方并不多。2.2 从编译脚本理解构建系统TF-A的构建系统是精修的Makefiles体系顶层Makefile是入口。通常的编译方式是make PLATversal BL33/path/to/u-boot.bin DEBUG1 V1 all这个命令里几个关键参数PLAT指定平台目录比如versal对应Xilinx versal平台rk3588对应Rockchip平台fvp是ARM官方提供的固定虚拟平台。BL33指定Non-Secure侧下一级镜像路径。ARM标准流程里BL33通常是U-Boot或UEFI。DEBUG设为1时会生成带调试符号、开启LOG_LEVEL高等级输出的版本。V1显示完整编译命令排查编译错误时非常有用。编译产生的镜像文件bl1.binBL1镜像烧录到片内SRAM或可信ROM区域。bl2.binBL2镜像。bl31.binBL31镜像完全独立的裸机固件。bl32.bin如果配置了OPTEE则产出。fip.bin将BL2、BL31、BL33等打包在一起的FIP格式镜像。bl1-bl2.bin一种BL1BL2合并镜像烧录方案更简单。我强烈建议你用DEBUG1做开发调试用LOG_LEVEL50查看详细日志。但要注意正式发布版本务必关掉DEBUG否则日志输出本身就是严重的信息泄露风险。2.3 启动流程中的信任链传递机制ATF的启动流程核心就是“分级加载、逐级认证”。这套机制是我审计时花时间最多的部分因为它直接决定了“到底什么能信”。整体思路很简单每一级引导程序在加载下一级之前都必须先对下一级的镜像做安全校验校验通过才会跳转。这形成了一条信任链芯片BootROM是第一信任根它验证BL1。BootROM的代码在硅片出厂时固化了无法修改理论上最可信。BL1验证BL2。BL1中内置了平台根公钥的hash用这个公钥验证BL2的签名。BL2验证BL31、BL32、BL33。BL2中包含了证书解析和验签逻辑。用生活化类比就是银行金库的钥匙在手开门验证押运员BL2验明正身后再让他开第二道库门放行长进来BL31行长再验证押运的那箱钱是不是真钞BL33。实际代码里bl2_plat_handle_post_image_load等接口就是BL2完成镜像加载后的挂载点。如果你看到这些接口里有额外的平台校验逻辑要注意是否弱化了原有的安全策略。2.4 关键数据结构上下文切换与Boot LayoutATF里最核心的数据结构一是cpu_context二是boot_params。cpu_context保存了CPU寄存器状态用于EL3与Normal World非安全侧之间的切换。你可以把它想成一个银行金库的交接记录表每次访问金库进EL3和离开金库回Non-secure都要记录当前状态下次回来能快速恢复现场。boot_params则描述了每个BL阶段的加载位置和大小。在ARMv8下BL1通常放在片内SRAM或特殊的ROM地址BL2可能放在SRAM或DDR可信区域BL31常驻DDR保留区域。平台移植时你最重要的任务之一就是合理规划这些内存布局。以RK3588为例BL31运行在DDR顶部保留的特定区域BL2则放在DDR安全区域。如果内存布局没规划好轻则启动卡死重则某个安全功能静默失效——后一种情况尤其危险。3. 安全固件工程审计要点与实操方法3.1 审计清单从代码层面找问题经验来看工程级的安全审计不必像安全研究那样追求0-day发现更应该关注“可用性、配置合理性和最小化攻击面”。按这个目标我整理的审计重点如下默认配置是否开启完整的安全启动链每个SMC接口是否做了调用来源校验即调用者的Safety/EL级别全局变量是否都初始化是否有敏感的调试打印平台内存映射表MMU Table是否存在过度授权的映射区域是否有绕过认证后直接加载镜像的代码路径BL31中是否存在未使用的、但暴露给Normal World的调试接口PSCI API是否符合ARM FFH规范是否暴露了不必要的扩展接口我习惯用grep快速搜索危险调用模式比如ssh相关的打印、callback、debug等关键词然后逐步跟踪函数调用路径。但是单纯grep很难发现逻辑漏洞更有效的方式是分别对关键函数走“白盒阅读动态验证”。3.2 信任根与固件验证机制的深度解析信任根通常以两种形式存在一是芯片BootROM内的固定公钥二是BL1内部的公钥hash即建立信任根的公钥列表。在ATF源码中BL1的验证逻辑定义在drivers/auth/目录下的认证框架中。在认证的过程中trusted board bootTBB配置很关键。当你开启TRUSTED_BOARD_BOOT编译选项时BL2会通过auth_mod_verify_signature对所有加载镜像执行RSA或ECDSA验签。验证内容包括镜像头格式是否正确。签名是否有效基于平台证书的验签。镜像哈希是否与证书中声明一致。镜像是否过期或不在允许的版本序列如果是防回滚场景。代码里plat_get_rotpk_info就是平台获取Root Of Trust Public Key的接口。这里坑很多——有些平台对可重编程存储区比如eMMC RPMB分区的信任处理得很模糊如果你把ROTPK存在可写存储中攻击面就被放大了。3.3 审计发现的典型安全薄弱点在这类审计中我总结出几类容易被忽略的风险点这里列一下调试接口许多原厂代码在定义平台宏时保留了大量调试日志包含DDR初始化Details、内存地址、启动参数。攻击者如果拿到串口访问权限这些信息能显著降低攻击门槛。SMC参数未校验完全BL31里的某些SMC接口对参数来源的exception level判断不严格或缺少对SMC调用者身份caller_sec_state的校验。理论上Normal World的普通应用EL0也可能触发某些本应只允许EL1/EL2调用的接口。信息泄露BL31处理SMC失败时部分错误码直接回显寄存器内容或缓冲区内容可能导致地址布局泄露。内存设置RO与RW权限混乱有些平台为省事将BL31的部分数据段映射为可读写但这些数据一旦可被写入可能覆盖安全策略表或跳转指针。未禁用的弱密码算法部分代码还残留对SHA-1的支持虽然未必被实际使用但如果你审计粒度不足很容易忽略。从工程角度说我建议做审计时同步开启Secure Boot并在目标板上跑一轮TF-A Tests或者直接跑Linux内核的KASAN级别压力。只有动态静态结合才能发现这类交叉问题。3.4 一份适合团队执行的审计流程参考审计不是一个人天马行空读代码建议按下面的流程来先读文档docs/目录下的firmware-design.rst、trusted-board-boot.rst、以及platform-porting-guide.rst是必读。这一层先建立全貌。拉一个干净的tag版本不要审计一堆本地修改CI拉主线或厂商release tag即可。按启动顺序逐模块审查BL1 → BL2 → BL31 → SPD/TEE。对每个平台函数plat_xxx标出调用点并记录函数职责。采用code review工具如Gerrit、GitLab MR review把审计过程记录下来方便追溯。最后跑实际硬件验证启动、休眠唤醒、热插拔CPU、TEE功能、安全启动失败注入测试。另外ATF官方有一个docs/下“security center”的指引整理了最近公布的漏洞列表建议每隔几个月对照检查自己平台是否需要同步更新补丁。4. 平台移植完整落地指南4.1 移植的前置条件和环境准备做ATF平台移植你手上最好先有这几样东西缺一不可目标板的芯片手册特别是存储映射、GIC配置、串口基地址、DDR控制器寄存器。原厂提供的可启动uboot镜像和内核镜像用来做对照验证。一块能稳定供电、有串口输出的目标板。JTAG调试器强烈建议准备启动阶段的串口输出来得太晚。主机的交叉编译工具链通常是aarch64-linux-gnu-前缀的GCC工具链。如果有TrustZone调试工具如ARM DS、JTAG会在调试时省很多时间。环境上在Ubuntu 22.04这类常见发行版上安装工具链即可sudo apt update sudo apt install gcc-aarch64-linux-gnu make device-tree-compiler git clone https://github.com/ARM-software/arm-trusted-firmware.git建议选择稳定的版本如v2.8、v2.9、v2.10或厂商维护分支。如果你用的是老平台直接切主线可能编译不过保持“够用即可”的策略。4.2 新建平台目录和最小启动配置TF-A平台代码的骨架非常简单核心是在plat/vendor/board/下创建你的平台目录并实现一组固定的平台接口。最小化平台目录结构通常是plat/mycompany/myboard/ ├── platform.mk ├── myboard_def.h ├── myboard_common.c ├── myboard_bl2.c ├── myboard_bl31.c └── myboard_helpers.S第一步看platform.mk里面指定了平台编译选项、包含路径、源文件列表。一个最基本的内容大致如下PLAT_INCLUDES : -Iplat/mycompany/myboard/include PLAT_BL_COMMON_SOURCES : \ plat/mycompany/myboard/myboard_common.c \ plat/mycompany/myboard/myboard_helpers.S \ drivers/arm/gic/v3/gicv3_main.c \ drivers/arm/gic/v3/gicv3_helpers.c BL2_SOURCES \ plat/mycompany/myboard/myboard_bl2.c BL31_SOURCES \ plat/mycompany/myboard/myboard_bl31.c第二步实现平台初始化函数比如platform_setup、plat_get_next_bl_params、plat_get_stack_info等。对调试来说第一步要保证串口输出能工作。以ARM PL011串口为例void bl31_early_platform_setup2(u_register_t arg0, u_register_t arg1, u_register_t arg2, u_register_t arg3) { /* UART base address from platform definition */ console_pl011_register(PLAT_MYBOARD_UART_BASE, PLAT_MYBOARD_UART_CLK_IN_HZ, PLAT_MYBOARD_UART_BAUDRATE, console_data); }这里console_pl011_register就是把寄存器基址注册成标准console。很多新手卡在没有输出其实就是这一步没做对或者基址不对或者UART时钟频率算错了。这个频率参数要查芯片手册的UART时钟树不是随便写的。4.3 平台内存布局规划与中断控制器配置内存布局规划是移植的核心它直接决定你能不能正常进入BL31。ATF通过链接脚本.ld.S来布置镜像段平台头文件里定义各镜像加载基址。我的建议是BL1尽量放在片内SRAM因为DDR控制器还没初始化代码必须能在不含DDR的环境中运行。BL2可以放在SRAM也可放DDR的安全区域但BL2运行前DDR已经可用BL1初始化完DDR。BL31常驻DDR安全区域这个区域必须在内存控制器上被保护防止Non-secure侧的Linux意外改写。以RK3588为例它的ATF代码中会把BL31放在系统DRAM的高端保留区。你在看代码时如果看到#define PLAT_RK3588_TRUSTED_SRAM_BASE这类宏就是这个用途。中断控制器方面ATF BL31需要把GICGeneric Interrupt Controller配置好因为PSCI的CPU热插拔、系统挂起/恢复都依赖于GIC的正确初始化。GIC版本以v3为主初始化逻辑包括设置GICD_CTLR的Group0和Group1使能。配置SPIShared Peripheral Interrupt中断的路由和触发方式。配置GICC/GICR基址。代码里主要是gicv3_driver_init和gicv3_distif_init函数。如果GIC配错系统一进BL31就直接死掉。4.4 标准开机已验证从编译到进U-Boot给大家一个我常用的最小验证路径编译ATFmake PLATyourplatform DEBUG1 V1 BL33/path/to/u-boot.bin all确认产物bl31.bin、fip.bin存在。烧录按厂商工具烧到启动介质可能是SPLeMMC分区或FIP容器。通过串口观察启动日志。如果一切正常你会看到类似这样的日志NOTICE: BL31: v2.9(release):v2.9 NOTICE: BL31: Built : ... INFO: BL31: Initializing runtime services INFO: BL31: Preparing for EL3 exit to normal world INFO: Entry point address 0x...然后U-Boot打印出现。到这一步平台移植的最小闭环已经完成。4.5 常见移植踩坑实录坑1串口只出BL1日志BL2卡死。大概率是BL2的DDR初始化或内存访问异常。检查BL2的bl2_platform_setup是否访问了尚未就绪的控制器或者SRAM布局重叠。坑2BL31进入后立刻重启。多数是GIC或定时器配置问题。建议在BL31初始化代码里逐步加NOTICE日志找到具体挂点。坑3U-Boot起来但Linux不能启动。往往不是ATF问题而是传给内核的DTB或启动参数不对但背锅的往往是ATF。先用U-Boot的booti命令单独验证内核能否启动。坑4PSCI调用返回错误码。核心是BL31是否注册了PSCI服务。看启动日志里Registered PSCI信息没有的话多半是条件宏没配置对。坑5安全启动验证失败设备变砖。这种场景要非常小心务必开发阶段先关闭验签验证完再开启。并且保留烧录工具能从MaskROM模式恢复的能力。5. 深入实战从审计需求到自定义扩展常见问题与排查5.1 如何扩展一个自定义SMC服务很多时候默认的ATF无法满足业务需求要在EL3加一个自定义SMC接口。举个例子比如你想实现一个只在安全世界可见的“设备唯一ID”读取接口。首先你要在BL31的服务框架里注册一个handler。代码长这样static uintptr_t my_smc_handler(uint32_t smc_fid, u_register_t x1, u_register_t x2, u_register_t x3, u_register_t x4, void *cookie, void *handle, u_register_t flags) { switch (smc_fid) { case MY_SVC_GET_UID: /* 做一些安全检查比如确保调用来自EL1以上 */ if (!is_caller_secure(flags)) { SMC_RET1(handle, SMC_UNK); } write_my_uid_to_buffer(x1); SMC_RET1(handle, 0); default: SMC_RET1(handle, SMC_UNK); } } DECLARE_RT_SVC(my_smc_service, MY_SVC_START, MY_SVC_END, my_smc_handler);然后在Linux侧你可以通过smc指令直接发起调用或者用arm_smccc_smc接口从内核发起。扩展SMC接口时最重要的是做两件事一是把MY_SVC_START/MY_SVC_END的编号范围定义好不要跟ARM标准SMC分配段冲突二是在合法范围内明确调用者的权限和安全状态否则很容易开个后门。5.2 基于真实硬件排查启动流程故障这里分享一个在RK3588平台上的具体排查案例。现象是板子偶尔冷启动卡死热启动正常。我先从U-Boot、内核日志出发没找到线索转向ATF端。用JTAG读寄存器发现BL31在初始化GIC时卡在一个等待循环里。进一步阅读代码发现有一个平台宏控制GICR基址的偏移当芯片型号的core数量不同时会差一截。原厂默认配置按“16核”去初始化但该板子实际只有8核结果访问了不存在的GICR frame导致总线hang。修复方法是把平台宏改成根据DTB解析CPU数量让GICR基址随核心数偏移。这种情况是典型的“默认能跑但特定硬件才暴露”的平台问题也正是为什么每一步启动日志和JTAG都不可少的原因。5.3 常用调试工具与日志分析方法ATF代码库内建了NOTICE、WARN、ERROR、INFO等日志级别。编译时通过LOG_LEVEL控制输出等级一般#define LOG_LEVEL 50 /* 输出所有消息 */在BL31中打印时可以用fmt字符串加参数。要注意在BL1阶段DDR未初始化串口打印受限于BL1所在介质通常是片内SRAM的驱动代码某些外设如DMA不能用。硬件调试方面强烈推荐使用JTAG比如ARM官方的Arm Development Studio或开源调试器OpenOCD配合gdb。可以做到(gdb) target remote :3333 (gdb) hbreak bl31_main (gdb) continue (gdb) info registers在启动早期一条指令就能让整个系统挂死所以打印日志是基础JTAG才是“重型武器”。5.4 平台移植后的必要测试清单移植完成不是终点我个人测试时会至少覆盖下列场景冷启动和热启动各100次确认无偶发问题。进入和退出Suspend系统休眠200次确认PSCI路径稳定。CPU热插拔操作确认CPU0以外的核能稳定上线和下线。TEE功能比如OPTEE启动、TA加载来回切换确认BL31与BL32的交互正常。非安全世界恶意SMC调用测试确认异常接口返回错误而不是挂死。开启安全启动后做一次签名错误注入测试确认设备拒绝启动并进入恢复模式。每次测试都要记录日志偶尔会把LOG_LEVEL调到最高跑一轮排查掉潜在信息过载或格式问题。5.5 常用问题排查速查表现象可能原因建议排查步骤BL1启动卡死SRAM初始化、串口时序检查JTAG读PC值确认卡在哪条指令BL2加载失败FIP打包错误、加载地址重叠用fiptool查看FIP内容核对BL2基址BL31重启GIC初始化错误、定时器配置错误分阶段加日志确认挂点PSCICPU_ON失败未正确配置电源域、GICR基址错误检查代码中的psci_validate_power_state实现安全启动验签失败ROTPK不匹配、根证书不正确检查BL1中ROTPK是否与烧录证书对应内核启动后无法访问TEESPD未编译进BL31确认编译时SPD选项正确比如SPDopteed板子变砖BL31损坏或验签失败从MaskROM/串口恢复模式重刷开发期务必保留无校验版本这张表是我每次换新板子时都会拿出来过一遍的检查清单所有端口、分区、烧录路径都是按厂商手册逐一核对的不能凭经验猜。6. 末尾随想与经验拾遗说一组我个人的小技巧。很多人面对ATF这种数千行C/汇编的代码库会有畏难情绪我也一样。但真正上手后会发现它其实遵循一个很清晰的模式每个BL阶段都有固定的setup流程平台代码只要按接口填空就行。心态摆正以后阅读速度会快很多。第二个实用习惯是“保持与主线同步”。即使你在做厂商分支我也建议每隔几个月拉一次上游TF-A的更新对比一下安全补丁。很多厂商分支长期不合并主线的安全修复这在产品生命周期很长的情况下尤其危险。第三个建议是设计上尽量把安全相关的逻辑集中在一个独立的模块里不要在平台代码里到处发散。比如你要加自定义SMC服务就在services/下新建一个目录不要塞进plat/的某个c文件里。这样做的好处是审计、测试、复用都容易而且避免平台适配代码和业务逻辑耦合。最后送大家一句话ATF的文档和代码永远是最好的老师遇到问题先查docs/再搜代码最后再问社区或者厂商。这个过程本身就是提升嵌入式底层能力最有效的方式。希望这篇基于实践的拆解能帮你少走一点弯路。