
做平台安全固件这几年ATFArm Trusted Firmware是我见过最能让人又爱又恨的东西。爱的是它把整个安全启动、特权级切换、电源管理全部抽象得清清楚楚恨的是第一次接触时面对BL1、BL2、BL31、BL32这些名词加上不同厂商魔改出来的版本基本就是一头雾水。这篇文章我打算把ATF的源码结构、工程审计思路、平台移植步骤一次讲透。如果你正准备给一块新的SoC接ATF或者单纯想弄明白信任固件到底在启动流程里做了什么这篇应该能在你看完FVP自带的文档之前把你拉进正确的轨道。1. ATF到底在 TrustZone 生态里唱哪出大戏1.1 为什么需要EL3这一层Armv8异常模型与权限划分要理解ATF必须先理解Armv8引入的异常级别模型。EL0到EL3一共四个等级数字越大权限越高。操作系统内核跑在EL1虚拟化场景下Hypervisor跑在EL2而EL3只做一件事管理安全世界与非安全世界的切换并运行整个系统里最敏感的可信固件。很多人第一次看到ATF时会问安全监控Secure Monitor代码为什么不能直接放在内核里原因很简单如果EL3代码落入操作系统手里那TrustZone划出来的安全内存、密码密钥、可信应用就全部失效了。把安全监控独立成一段永不被Normal World访问的固件是物理隔离的第一道防线也是ATF存在的终极意义。安全世界和普通世界之间通过SMCSecure Monitor Call指令沟通。普通世界的代码想请求安全服务比如读取安全时钟、触发安全中断、或者执行电源状态切换都会执行SMC指令陷入EL3由ATF来决定要不要放行。这块逻辑有点像机场安检普通程序不能直接进隔离区必须先到安检口递申请通过了才能进去ATF就是这个安检口的全部工作人员。1.2 启动阶段逐级拆解BL1、BL2、BL31、BL32、BL33ATF把整个启动过程拆成五个阶段对应着BLBoot Loader编号。BL1是芯片出厂ROM里固化的一段代码上电后首先执行负责初始化最小硬件环境然后把BL2加载到SRAM并跳转过去。BL1之后BL2负责更完整的平台初始化并负责验证和加载后续的所有镜像。BL31是真正意义上的“常驻固件”整个系统运行期间EL3都是由BL31管理的它提供PSCI电源管理、SMC分发、以及安全世界切换服务。BL32是可选的TEE OS比如OP-TEE它运行在安全世界的EL1。BL33就是普通世界的引导程序最常见的就是U-Boot。整个链条下来很像一个接力赛ROM里的小个子BL1把接力棒交给SRAM里的搬运工BL2搬运工把包裹分好交给物业经理BL31物业经理再用最快的速度把门房保安TEE和前台引导BL33安排到位之后整个大楼的运行秩序就交给物业经理统一监督了。1.3 PSCI让内核和ATF对话的电源管理协议PSCIPower State Coordination Interface是ATF给外头提供的标准服务接口。为什么内核不能直接操作CPU关停、休眠、重启因为很多电源操作涉及Secure World的状态普通世界没有权限只能通过SMC指令委托ATF处理。ATF里的PSCI实现同时充当协议翻译官和策略执行者收到内核发来的CPU_ON或CPU_OFF请求先检查参数再调用平台底层提供的高低电平操作、时钟门控、寄存器配置完成电源状态切换。在做平台移植时PSCI是最容易踩坑的部分。平台层要实现plat_psci_ops里面的回调例如pwr_domain_on、pwr_domain_off、pwr_domain_suspend等。如果你只是在QEMU上模拟也许随便填几个空函数就能跑但真实芯片上电源域的依赖关系、cache维护、中断保存恢复都有严格的先后顺序错一步就可能在唤醒瞬间直接跑到异常向量里去。2. 安全启动与信任链ATF的安全家底2.1 信任根与Chain of Trust设计思路安全启动最核心的概念是信任根Root of Trust。ATF沿用了TBBRTrusted Board Boot Requirements定义的信任链模型ROM里以不可变的方式固化了信任根的哈希值或公钥后续每个阶段的镜像都要用这把信任根去验证签名验证通过才放行。BL1用信任根验证BL2的证书BL2再用同样的链式结构验证BL31、BL32、BL33的证书。这条链一旦某环被篡改后面全部校验失败系统就不会启动。这样做的好处是即使存储介质被完全控制攻击者也伪造不出合法的镜像链。BL1本身在ROM里无法被改写这个物理不可变性是整个信任模型的基石。实际工程中平台需要把厂商公钥烧进OTPOne-Time Programmable区域。ATF代码里对应的配置项是ARM_ROTPK_LOCATION和ARM_ROTPK_HASH具体烧录时会用到cert_create工具生成的公钥哈希。很多团队第一次移植时图省事直接关掉TBBR让ATF裸跑这在量产板上是不可接受的安全启动必须保持使能。2.2 证书链、认证模块与fiptool认证过程依赖X.509证书和多个扩展字段。ATF源码里tools/cert_create负责生成证书tools/fiptool负责把BL21、BL31、BL32、BL33以及证书打包成一个FIP文件。FIP相当于一个固件归档包BootROM里的BL1只需要知道哪个地址放FIP头然后按偏移取出对应镜像即可。drivers/auth目录里的认证框架是整个校验流程的核心它把证书解析、签名验证、哈希比较抽象成统一接口。默认使用Mbed TLS实现RSA、ECDSA、SHA256等算法你也可以替换成自研的密码库。这里有个细节容易被忽略认证模块有自己的内存分配策略大证书或者高并发验证时可能撑爆BL2的堆空间必要时需要调大Mbed TLS的堆大小配置。2.3 度量启动与未来方向除了解密验证型信任链ATF还支持度量启动Measured Boot也就是把每个阶段的镜像哈希逐级记录到TPM或其他可信存储中外部校验设备可以事后审计启动过程。虽然这块在标准ATF主线上已经有不少扩展真正落地时还要看具体安全方案的集成度。近几年Arm在RSSRuntime Security Subsystem、CCAConfidential Compute Architecture上动作很大ATF的职责也在慢慢扩张。原来单纯由EL3固件承担的可信根功能正逐步向独立的协处理器转移。做长期规划时手里的老平台可能暂时用不到这些特性但设计新平台时最好留好接口。3. 源码工程审计把ATF仓库翻个底朝天3.1 顶层目录结构与各模块职责ATF源码的顶层目录紧凑到让人惊讶。bl1、bl2、bl31、bl32分别是各启动阶段的主体common放的是主体间共享的镜像描述和加载逻辑drivers放各平台外设驱动常见的有console、gic、io、delay_timer、authlib放的是编译期静态库比如el3_runtime、psci、xlat_tables、cpus、fconfservices则是各种运行时服务包括std_svc、arm_arch_svc、spd等plat是平台相关目录放每家SoC的板级代码。这个老仓库虽然年岁不小但分层比较清楚平台无关的框架和平台相关的钩子被严格分开上层想加一个新电源状态不需要去翻芯片手册只需要在平台目录里实现规定的回调。从工程审计的角度看我最喜欢看plat目录的差异因为厂商对ATF的二次开发基本都沉淀在这里代码质量高低一眼就能看出来。3.2 一个典型BL31的启动流程代码走读BL31的入口在bl31/bl31_main.c里的bl31_main函数。它一上来先把运行时服务列表逐项初始化包括early_platform_setup、plat_arch_setup、bl31_platform_setup等钩子。接下来通过runtime_svc_init注册所有服务这份服务表由编译期宏展开生成每个服务包含一个唯一ID和对应的SMC分发回调。最后BL31调用bl31_prepare_next_image_entry把CPU交给BL32或BL33进入正常世界。整个流程安排得很克制没有把所有初始化逻辑都塞在同一个文件里。阅读源码的时候只需盯住这几个关键函数就够看清BL31的骨架了。如果你是从安全固件审计的角度进仓库重点应该放在runtime_svc_init后面那张SMC分发路由表上恶意篡改固件时非常喜欢在这层做文章。3.3 驱动与库console、GIC、cache-erratadrivers/console是移植初期最关注的子系统。ATF支持的串口模型很多有pl011、16550、cdns等。串口初始化在极早期就要完成因为后续所有的报错日志都依赖这一路输出。GIC通用中断控制器驱动则要处理安全中断和非安全中断的路由。lib/cpus里面则是为各类Cortex核准备的errata处理代码。每个SoC运行时都会根据CPU的版本信息应用对应的EENER有些老核的ERRATA需要额外在平台启动时调用。若与GCC/armclang版本不匹配有些errata代码可能会被编译器优化掉导致诡异崩溃。补一句经验在源码里看到#if ERRATA_XXX时尽量不要图省事一把关掉哪怕当前芯片版本不涉及也要保持宏定义完整。3.4 编译系统的设计ATF的构建系统建立在GNU Make之上顶层Makefile通过PLAT指定目标平台例如make PLATqemu DEBUG1。编译产物全部集中在build/plat/debug|release/下包括bl2.elf、bl31.elf、bl32/tee.elf以及用于打包的fiptool。很多人在新手上路时习惯直接摸Makefile我反倒建议先看各个平台下的platform.mk那里写清楚了该平台启用了哪些源文件、哪些宏定义比从零追踪链式make规则高效得多。toolchain方面ATF通常要求armv8-a架构的裸机编译器。官方推荐使用arm-none-eabi-gcc或Armclang。如果你还在用老旧的armcc 5.06之类的工具链建议尽早换到官方长期支持版本否则某些新写的汇编或ld脚本特性编译不过去排查半天发现是编译器版本限制了语法实在太冤。4. 平台移植落地从零给一块新板子接上ATF4.1 先说结论抄一个最接近的参考平台平台上手第一步不是写新代码而是选一个参考平台。如果你手头是一颗Cortex-A5x/A7x的自研芯片最理想的参考是plat/arm/board/fvp或plat/qemu如果你做的是ARMv8.2的新架构plat/arm/board/juno也值得看。大多数厂商的BSP还会额外保留一个plat/vendor/soc目录专门给下游适配这正是可复用的起点。把参考平台复制一份之后先不要急着改文件内容而是把所有宏定义梳理一遍。需要改的地方主要集中在内存基址、UART基址、GIC基址、电源拓扑、BLxy的加载地址。这步做得越细致后续调试越省心。4.2 建立平台目录与platform.mk在plat/下新建独立目录后至少要准备platform.mk、plat_common.c、plat_setup.c、plat_pm.c、plat_topology.c以及对应的头文件。platform.mk用来声明源文件列表和额外编译宏比如UART实例、内存基址、是否启用TBBR等。我习惯把每个半导体厂商或板卡型号作为一个子目录层级比如plat/vendor/board/。这样后续维护多个板型时目录结构不用推倒重来。曾经见过某些项目把三个板子的配置全写进一个platform.mk里条件编译满天飞每次改板卡都像拆雷最后只能重写。4.3 内存布局与MMU配置内存布局是ATF移植最核心的部分。平台需要告诉BL31安全RAM的基址和大小例如BL31_BASE和BL31_LIMIT同时配置BL2、BL32各自占用的地址区间。这些值在include/plat/arm/common/arm_def.h和平台头文件里统一定义。如果布局写错轻则U-Boot起不来重则直接在启动阶段触发异常同步错误。MMU配置由xlat_tables库完成。ATF内部有自己的页表翻译实现平台层通过mmap_add_region注册安全内存、设备内存等区域。我给一个新平台配MMU时一贯原则是代码段、数据段、设备寄存器区间统统只给必要权限能开设备内存就开设备内存尽量不要把内核整块物理内存映射成安全霸占否则运行时安全检查会把你拦住。4.4 最小启动Bring-up串口、GIC、BL33加载第一次Bring-up不需要太多功能目标就是让BL2把BL31和U-Boot装起来并看到串口日志。建议先关闭TBBR校验保留最基础的BL31运行这样能把问题范围缩小。串口初始化要在BL2早早期完成ATF里的日志宏ERROR、WARN、NOTICE都在串口ready之后才有可能输出。GIC配置可以先用最简单的模式安全中断路由到EL3非安全中断路由到EL2/EL1等基础系统跑通再精细化。4.5 构建、打包与烧录流程构建产物拿到手后需要用fiptool create把BL2以外的所有镜像打成FIP。打包命令类似make PLATqemu DEBUG1 tools/fiptool/fiptool create --tb-fw build/qemu/debug/bl2.bin \ --soc-fw build/qemu/debug/bl31.bin \ --nt-fw u-boot.bin fip.bin生成fip.bin后把它烧录到BL1约定的存储介质偏移处。需要注意BL1从固定偏移读取FIP头这个偏移在厂商BootROM文档里规定。烧录时还要确保FIP镜像本身对齐部分BootROM对对齐很敏感差一两个字节就可能触发“找不到FIP”的报错。5. 常见问题与排查技巧实录5.1 串口无输出或乱码遇到完全无输出的情况第一批排查点是UART基址是否配错、时钟频率是否和驱动里标称一致、引脚复用是否被早期BootROM占用。乱码则多半是波特率偏差太大或者驱动使用了错误的分频基准。靠谱的办法是先用逻辑分析仪抓物理电平确认TX引脚上确实有信号再回头查软件。5.2 FIP镜像加载失败/签名校验失败如果BL2能打印FIP读取失败先查FIP头偏移和烧录地址。如果打印签名校验失败而我确认TBBR已经关闭那就该检查ARM_ROTPK_LOCATION之类的宏是否还指向旧配置或者BL2有没有从错误地址读取了残留证书。5.3 BL31崩溃与ELF转储分析BL31运行崩溃时串口通常会打印异常类型、ELR、SP、LR等寄存器。ATF的crash报错已经算友好但你需要能读懂ELR是出错的PC地址通过反编译build/plat/debug/bl31.elf定位到具体函数再把当前SP地址和栈布局对照。建议编译时保留debug符号否则就只能对着反汇编猜了。5.4 多核唤醒与断电状态切换异常多核平台上secondary core上电后往往要经过一段独立启动流程ATF提供plat_secondary_cold_boot_setup钩子。如果二级核一直起不来先确认mailbox地址和同步变量是否落在安全RAM内再看GPIO或电源控制器的依赖关系。核心休眠后唤醒异常则要注意cache maintenance操作的顺序有些坑是缓存没刷干净CPU被唤醒后执行的还是旧指令。5.5 编译器版本与构建环境常见坑ATF对编译器的版本敏感度比想象中高。某些老内核、老板卡BSP与新版ATF搭配时会因为内联汇编语法差异编译失败。如果你手上既有一堆老工程又必须碰新版ATF最好用容器固定工具链版本别让本机GCC升级把整个构建链弄炸。还有个小坑ATF对DEBUG1和LOG_LEVEL的组合很讲究BL2阶段看不到日志时优先调大LOG_LEVEL并确认串口控制台已注册。我在实际项目的体会是ATF移植最忌讳一上来就追求“全功能”。先把BL1到BL33这条最细的链路跑通把串口、内存、中断这三驾马车理顺再去开TBBR、SPD、TEE这些高阶功能。调试安全固件不像普通应用可以打日志打得很随意很多现场只靠一条串口的输出判断启动到哪一步所以日志分级、地址规划这些基本功反而比炫技代码更值钱。最后再分享一个让我记忆深刻的教训有个平台在U-Boot阶段一切正常但进入Linux后随机性死机。排查了很久才发现根因是BL31阶段使用到的某段设备内存没有被映射上Linux内核通过FF-A调用发生异常时跳回EL3直接访问了非法地址。从那以后我每次给新平台写MMU映射表时都会把安全世界外的设备区间也详细列出来宁可让BL31跑起来速度慢一点也不给它留访问空洞。这种经验没人写进文档只能靠板子前一次次崩溃换回来。