3、ATF基础架构:BL1/BL2/BL31/BL32/BL33阶段划分,SMC调用与安全监控模式
ARM Trusted Firmware,圈内人都叫它ATF。这东西说白了就是ARMv8-A架构下安全世界的“地基”。我刚开始接触它的时候,也被那一堆BL给绕晕过。BL1、BL2、BL31、BL32、BL33……这到底谁先谁后?各自干什么?
今天我们就把它彻底理清楚。你想想看,一个系统从上电到进入操作系统,中间要经历多少道关卡?ATF就是负责把这些关卡的安全门都管好。
3.1 启动流程总览:五阶段接力赛
ATF的启动是一个典型的“接力棒”模型。每个BL阶段完成自己的初始化后,把控制权交给下一个阶段。我习惯把这五个阶段画成一张图,这样一看就明白。
核心概念:ATF的启动顺序是 BL1 → BL2 → BL31/BL32 → BL33。BL31和BL32是并行加载的,但BL31先运行。
3.2 各阶段深度解析
3.2.1 BL1:第一级启动,ROM里的“铁钉子”
BL1是芯片出厂时固化在ROM里的代码。它不可修改,不可擦除。我个人觉得,BL1的设计哲学就是“最小可信基”。它只做两件事:初始化最基本的硬件(比如时钟、SRAM),然后加载并验证BL2。
为什么BL1要放在ROM里?因为ROM是只读的,攻击者改不了。这是整个信任链的根。我记得在某个项目中,芯片的ROM有bug,导致BL1在特定温度下读取Flash会出错。那叫一个头疼——ROM没法打补丁,最后只能靠BL2做workaround。
避坑指南:BL1的代码量通常控制在4KB-16KB以内。空间极其有限,别想着在BL1里做复杂的事情。我曾经见过有人试图在BL1里加USB驱动,结果ROM装不下,整个项目推倒重来。
3.2.2 BL2:可信启动的“守门员”
BL2是存放在Flash中的。它负责加载并验证BL31、BL32、BL33的镜像。验证方式通常是数字签名校验——用公钥验签,确保镜像没有被篡改过。
BL2运行在EL3(安全最高权限级别)。它做完验证后,会把控制权交给BL31。这里有个细节:BL2在交出控制权之前,会把自己占用的内存清理干净,防止敏感信息泄露。
| 阶段 | 存储介质 | 异常级别 | 主要职责 |
|---|---|---|---|
| BL1 | ROM | EL3 | 最小硬件初始化,加载BL2 |
| BL2 | Flash | EL3 | 加载并验证后续所有镜像 |
| BL31 | SRAM/DRAM | EL3 | 运行时服务,SMC处理,PSCI |
| BL32 | SRAM/DRAM | S-EL1 | 安全操作系统(如OP-TEE) |
| BL33 | DRAM | EL2/EL1 | UEFI或操作系统内核 |
3.2.3 BL31:EL3的“大管家”
BL31是ATF的核心。它运行在EL3,负责处理安全监控模式下的各种请求。说白了,BL31就是安全世界和普通世界之间的“交换机”。
BL31提供的主要服务包括:
- PSCI(电源状态协调接口):CPU的休眠、唤醒、开关机都由它管。
- SMC处理:接收来自普通世界的安全调用请求。
- 中断路由:决定一个中断是交给安全世界处理,还是交给普通世界。
BL31启动后,会初始化中断控制器(GIC)、系统计数器等。然后它进入一个等待循环,随时准备处理SMC调用或中断。
3.2.4 BL32:安全操作系统(可选)
BL32是一个可选的安全操作系统。最常见的是OP-TEE(Open Portable Trusted Execution Environment)。它运行在S-EL1,提供安全服务,比如密钥管理、安全存储、加密运算等。
为什么需要BL32?因为BL31虽然权限高,但功能太单一。如果你需要运行复杂的可信应用(TA),就需要一个真正的操作系统来管理。BL32就是干这个的。
注意:BL32不是必须的。如果你的系统不需要安全操作系统,可以只跑BL31+BL33。但这样做的话,所有安全服务都得在BL31里实现,代码会变得很臃肿。我个人建议,只要硬件支持,就加上BL32。
3.2.5 BL33:最终启动目标
BL33就是UEFI或者操作系统内核。在UEFI+ATF的协同场景下,BL33通常是UEFI固件。UEFI运行在EL2(虚拟化扩展)或EL1,通过SMC调用向BL31请求安全服务。
3.3 SMC调用:安全世界的“门铃”
SMC(Secure Monitor Call)是普通世界进入安全世界的唯一入口。你想想看,普通世界的代码权限低,不能直接访问安全资源。那它怎么让安全世界帮忙做事?按门铃——发一个SMC异常。
SMC调用的流程是这样的:
- 普通世界执行
SMC #0指令。 - CPU陷入EL3,BL31的SMC处理函数被调用。
- BL31解析SMC参数,判断是哪个服务(PSCI、TOS、自定义)。
- BL31执行对应的处理逻辑,或者转发给BL32。
- 结果通过寄存器返回给普通世界。
这里有个关键点:SMC调用是有标准规范的。ARM定义了SMC Calling Convention(SMCCC),规定了参数怎么传、返回值怎么取。我建议你直接看SMCCC文档,别自己瞎猜。
// 一个典型的SMC调用示例(UEFI侧)
// 调用PSCI_CPU_ON,唤醒CPU1
UINT64 FunctionId = 0xC4000003; // PSCI_CPU_ON的SMC ID
UINT64 CpuId = 1;
UINT64 EntryPoint = 0x80000000;
UINT64 ContextId = 0;
// 执行SMC指令
// 在UEFI中通常通过封装好的函数调用
Status = ArmCallSmc(FunctionId, CpuId, EntryPoint, ContextId, NULL);
关键点:SMC调用的开销大约是几百个CPU周期。虽然比普通函数调用慢,但比中断快得多。在性能敏感的场景下,尽量减少SMC调用次数。我习惯的做法是批量处理——一次SMC调用完成多个操作。
3.4 安全监控模式:EL3的“上帝视角”
安全监控模式(Secure Monitor Mode)是ARMv8-A引入的概念。它对应EL3异常级别。在EL3下,代码可以访问所有系统资源——安全世界的、普通世界的,通通都能看。
这种“上帝视角”有什么用?
- 上下文切换:当普通世界和安全世界切换时,EL3负责保存和恢复寄存器状态。
- 中断路由:EL3可以配置GIC,决定一个中断是触发安全中断还是普通中断。
- 系统控制:比如电源管理、复位控制等,这些操作必须在最高权限下执行。
我记得有一次调试一个中断路由问题。安全世界的定时器中断老是跑到普通世界去。查了半天,发现是GIC的配置寄存器在EL3下没设对。嗯,这种问题在EL3下调试特别麻烦——因为一旦EL3崩了,整个系统就挂了,连个日志都打不出来。
调试技巧:在BL31中加一个简单的UART输出函数。我习惯在EL3的SMC处理入口和出口各打一行日志。这样能快速定位是哪个SMC调用出了问题。别指望JTAG,很多量产板子没有JTAG接口。
3.5 各阶段的内存布局
内存怎么分?这是个大学问。ATF各阶段对内存的需求不同,而且必须互不重叠。我见过太多因为内存布局冲突导致的诡异bug。
| 阶段 | 典型内存区域 | 大小 | 说明 |
|---|---|---|---|
| BL1 | ROM + 内部SRAM | 4KB - 16KB | BL1代码在ROM中,数据在SRAM中 |
| BL2 | 内部SRAM | 64KB - 256KB | BL2运行期间独占 |
| BL31 | 内部SRAM 或 保留DRAM | 64KB - 512KB | 常驻内存,不能被OS覆盖 |
| BL32 | 安全DRAM | 4MB - 32MB | 安全OS和TA使用 |
| BL33 | 普通DRAM | 剩余所有 | UEFI或OS使用 |
这里有个容易踩的坑:BL31和BL32的内存必须从普通世界的视角“隐藏”起来。怎么做?通过内存控制器或系统MMU的配置,让普通世界根本看不到这些区域。否则,一个恶意程序就能直接读取安全世界的数据。
严重警告:千万不要把BL31或BL32的内存区域映射到UEFI的页表里。我曾经见过一个项目,UEFI的页表不小心包含了安全内存区域,结果一个DMA操作直接把安全OS的数据给覆盖了。系统跑着跑着就神秘重启,查了整整两周才找到原因。
3.6 小结
ATF的五阶段架构,说白了就是一条信任链。从ROM里的BL1开始,一级一级验证,一级一级传递信任。BL31是枢纽,SMC是通道,安全监控模式是保障。
理解这些基础,你才能知道UEFI和ATF到底怎么配合。下一层我们就要开始讲具体的协同启动了。