4. ATF(ARM Trusted Firmware)深度解析:BL1/BL2/BL31/BL32/BL33各阶段职责、SPM与FF-A协议
ARM Trusted Firmware(ATF),圈内人通常叫它TF-A。这东西说白了就是ARMv8/v9架构下安全世界的“地基”。我最早接触它是在一个手机SoC项目上,当时被BL1到BL33这一串启动阶段搞得晕头转向。后来调了几个月,才慢慢摸清每个阶段到底在干什么。
今天咱们就把ATF的五个启动阶段、安全分区管理器(SPM)以及FF-A协议彻底捋一遍。你想想看,整个系统从上电到进入操作系统,中间经历了什么?嗯,就是这五个阶段在接力。
4.1 ATF启动流程总览
ATF把安全世界的启动分成了五个阶段,编号从BL1到BL33。每个阶段都有明确的职责边界。我习惯把这五个阶段比作一场接力赛:
- BL1(Boot Loader Stage 1):ROM里的“第一棒”,上电即运行
- BL2(Boot Loader Stage 2):加载后续镜像的“调度员”
- BL31(Boot Loader Stage 3-1):安全运行时环境,常驻内存
- BL32(Boot Loader Stage 3-2):可选的安全操作系统(如OP-TEE)
- BL33(Boot Loader Stage 3-3):非安全世界的引导程序(通常是U-Boot)
关键认知:BL1和BL2只在启动阶段运行,运行完就释放内存。BL31、BL32、BL33则常驻系统,直到下次复位。
下面这张图展示了ATF的完整启动流程和阶段切换关系:
4.2 各阶段深度解析
4.2.1 BL1:ROM里的“第一行代码”
BL1是芯片上电后CPU执行的第一段代码。它固化在ROM里,不可修改。我记得第一次看BL1源码时,发现它做的事情极其精简:
- 初始化最基本的硬件(CPU、栈指针、异常向量表)
- 从Flash/EEPROM中加载BL2到SRAM
- 验证BL2的签名(如果使能了安全启动)
- 跳转到BL2入口
注意:BL1的代码量通常被限制在几十KB以内。我曾经在一个项目中因为BL1代码超了ROM大小,不得不砍掉一些调试功能。所以写BL1时,能省则省。
4.2.2 BL2:镜像加载的“总调度”
BL2运行在SRAM中,它的主要工作是加载并验证后续所有镜像。我个人觉得BL2是整个启动流程中最关键的一环——它决定了系统能不能正确启动。
BL2的核心职责:
- 解析FIP(Firmware Image Package)包,找到各镜像的位置
- 将BL31、BL32(可选)、BL33加载到指定内存地址
- 验证每个镜像的签名和哈希值
- 配置内存保护单元(如TZC-400)
- 跳转到BL31
经验之谈:调试BL2时,建议先关闭签名验证,等所有镜像都能正常加载后再开启。否则签名失败连错误日志都看不到,排查起来非常痛苦。
4.2.3 BL31:安全世界的“常驻管家”
BL31是ATF的核心组件。它不像BL1/BL2那样用完就丢,而是常驻在安全内存中,提供运行时服务。说白了,BL31就是安全世界和非安全世界之间的“看门人”。
BL31提供的关键服务:
| 服务类型 | 说明 | SMC功能ID范围 |
|---|---|---|
| PSCI(电源状态协调接口) | CPU热插拔、挂起、复位等电源管理 | 0x84000000 - 0x8400FFFF |
| SDEI(软件委托异常接口) | 安全中断的委托处理 | 0xC4000000 - 0xC400FFFF |
| FF-A(固件框架) | 安全分区间的通信管理 | 0x84000060 - 0x8400007F |
| 自定义服务 | 厂商特定的安全服务 | 0x84000100 - 0x840001FF |
BL31通过SMC(Secure Monitor Call)指令接收非安全世界的请求。当非安全世界想执行安全操作时,会触发一个SMC异常,CPU陷入EL3,由BL31处理。
4.2.4 BL32:安全操作系统(可选)
BL32是一个可选阶段。如果系统需要运行安全操作系统(如OP-TEE、Trusty),BL31会加载并跳转到BL32。BL32运行在S-EL1,提供安全服务给非安全世界的客户端。
我参与的一个支付终端项目就用了OP-TEE作为BL32。当时遇到一个坑:BL32的栈大小配置太小,导致安全应用一运行就崩溃。排查了两天才发现是栈溢出。
配置建议:BL32的栈大小至少配置8KB以上,如果安全应用复杂,建议16KB起步。
4.2.5 BL33:非安全世界的“入口”
BL33通常是U-Boot或EDK2。它运行在非安全世界(EL2/EL1),负责加载操作系统内核。BL31在完成初始化后,会通过ERET指令跳转到BL33的入口地址。
BL33的启动参数由BL31通过寄存器传递:
// BL31跳转到BL33时,寄存器传递的参数
x0 = BL33入口地址
x1 = 平台特定信息(如内存布局)
x2 = 设备树地址(如果使用设备树)
x3 = 其他平台参数
4.3 SPM(安全分区管理器)
SPM是ATF中一个相对较新的组件。它的出现是为了解决一个问题:多个安全服务如何安全地共享硬件资源?
SPM的核心思想是把安全世界划分成多个隔离的“分区”(SP,Secure Partition)。每个分区运行在自己的地址空间里,拥有独立的权限。SPM负责:
- 分区的创建和销毁
- 分区间的内存隔离
- 中断的路由和委托
- 分区间通信的仲裁
SPM有两种实现模式:
| 模式 | 描述 | 适用场景 |
|---|---|---|
| SPM_MM(内存管理模式) | 基于S-EL1分区,使用MMU隔离 | 传统ATF,兼容性好 |
| SPM_FF-A(FF-A模式) | 基于FF-A协议,支持S-EL0分区 | 新平台,灵活性高 |
我的建议:新项目直接上SPM_FF-A模式。虽然SPM_MM更成熟,但FF-A是ARM力推的方向,后续的扩展性更好。
4.4 FF-A(固件框架)协议
FF-A(Arm Firmware Framework for Arm A-profile)是ARM定义的一套标准协议,用于规范安全分区之间的通信。它解决了之前各厂商各自为政、互不兼容的问题。
FF-A的核心概念:
- Endpoint(端点):通信的参与者,可以是SP、OS内核或Hypervisor
- Partition(分区):一个隔离的执行环境,拥有自己的内存和权限
- Message(消息):端点之间交换的数据单元
- Direct Request/Response:直接请求/响应模式,用于同步通信
- Memory Sharing:内存共享机制,用于大数据传输
FF-A定义了一组标准SMC接口,所有兼容FF-A的固件都必须实现:
// FF-A标准SMC功能ID示例
#define FFA_VERSION 0x84000060
#define FFA_FEATURES 0x84000061
#define FFA_RX_RELEASE 0x84000065
#define FFA_RXTX_MAP 0x84000066
#define FFA_PARTITION_INFO_GET 0x84000070
#define FFA_MSG_SEND_DIRECT_REQ 0x84000071
#define FFA_MSG_SEND_DIRECT_RESP 0x84000072
我在一个多核服务器项目中用FF-A实现了安全分区间的通信。当时遇到一个性能问题:每次消息传递都要通过SMC陷入EL3,开销太大。后来改用内存共享+门铃机制,性能提升了近10倍。
性能陷阱:FF-A的Direct Request模式每次通信都需要两次SMC(请求+响应)。如果通信频率高,建议用内存共享模式,配合中断通知,能大幅降低延迟。
4.5 各阶段内存布局
理解ATF各阶段的内存布局,对调试和移植至关重要。下面是一个典型的内存布局示例:
| 地址范围 | 大小 | 用途 | 阶段 |
|---|---|---|---|
| 0x00000000 - 0x0000FFFF | 64KB | BL1代码+数据(ROM) | BL1 |
| 0x00100000 - 0x0011FFFF | 128KB | BL2代码+数据(SRAM) | BL2 |
| 0x00200000 - 0x0023FFFF | 256KB | BL31代码+数据(DRAM安全区) | BL31 |
| 0x00240000 - 0x0027FFFF | 256KB | BL32代码+数据(DRAM安全区) | BL32 |
| 0x00280000 - 0x003FFFFF | 1.5MB | 安全分区(SP)内存 | SPM |
| 0x00400000 - 0xFFFFFFFF | ~4GB | 非安全世界内存 | BL33/OS |
移植要点:修改内存布局时,一定要同步更新BL2的FIP解析逻辑和BL31的MMU配置。我曾经因为改了DRAM基地址但忘了更新BL31的页表,导致系统启动到一半就挂了。
4.6 实战:添加一个自定义SMC服务
最后,咱们看一个实际例子——在BL31中添加一个自定义SMC服务。这在产品开发中很常见,比如实现一个安全存储服务。
// 1. 在BL31中注册SMC处理函数
uint64_t custom_smc_handler(uint32_t smc_fid,
uint64_t x1, uint64_t x2,
uint64_t x3, uint64_t x4,
void *cookie, void *handle,
uint64_t flags)
{
switch (smc_fid) {
case 0xC4000100: // 自定义服务:安全存储写入
// 处理写入请求
return secure_storage_write(x1, x2, (void *)x3);
case 0xC4000101: // 自定义服务:安全存储读取
return secure_storage_read(x1, x2, (void *)x3);
default:
return SMC_UNKNOWN;
}
}
// 2. 在BL31初始化时注册
void bl31_platform_setup(void)
{
// 注册自定义SMC处理函数
register_smc_handler(0xC4000100, 0xC40001FF, custom_smc_handler);
}
嗯,ATF的内容确实不少。但只要你理解了这五个阶段的职责划分,再结合SPM和FF-A的通信机制,整个安全固件的架构就清晰了。实际项目中,大部分时间都在调BL31和BL32的交互,以及处理各种SMC请求的边界情况。