3. 源码结构解析
好,咱们直接进入正题。这一章我打算带你把ATF的源码目录翻个底朝天,再把BL1到BL33这几个启动阶段彻底讲透。说实话,我刚接触ATF那会儿,光看目录结构就晕了半小时——文件夹套文件夹,名字还都挺像。但等你真正搞明白每个目录是干嘛的,每个BL阶段负责什么,你会发现这玩意儿设计得其实挺巧妙。
3.1 ATF源码目录结构
先说说顶层目录。你从官方仓库拉下来代码后,第一眼看到的就是这些:
bl1/
bl2/
bl2u/
bl31/
bl32/
bl33/
common/
plat/
include/
lib/
docs/
tools/
嗯,看着是不是有点眼熟?bl1到bl33这几个目录,就是咱们后面要重点讲的启动阶段。每个目录里放的是对应阶段的源码和链接脚本。我个人的习惯是,先看plat/目录,因为平台相关的代码都在这里,移植工作主要就是改这里面的东西。
再往下看:
- common/:放的是各阶段共享的通用代码,比如一些基础的数据结构、内存管理工具。
- lib/:库函数目录,里面又有子目录,比如
lib/xlat_tables/是页表操作库,lib/psci/是电源管理接口库。这些库你移植时基本不用动,但得知道它们的存在。 - include/:头文件大本营。我建议你重点看
include/plat/,平台相关的宏定义和接口声明都在这里。 - docs/:官方文档。说实话,有些文档写得挺简略的,但
docs/porting-guide.rst这份移植指南你必须读一遍。 - tools/:一些辅助工具,比如生成证书、打包镜像的工具。
这里我插一句。我在做第一个移植项目时,犯过一个低级错误——直接改了common/目录下的代码。结果一编译,其他平台全崩了。后来才明白,common/是共享的,你改它会影响所有平台。正确的做法是在plat/里重写或覆盖。
common/、lib/ 等通用目录下的代码。平台定制请全部放在 plat/<平台名>/ 下。
3.2 BL1:最底层的“硬核”启动
BL1,全称Boot Loader Stage 1。它是芯片上电后执行的第一个代码,通常放在ROM里,不可修改。你想想看,这时候DRAM还没初始化呢,代码只能在SRAM或者Cache里跑。
BL1的核心任务就三件:
- 初始化最基本的硬件——比如关闭中断、设置栈指针。
- 验证BL2镜像的签名和完整性(如果开启了安全启动)。
- 加载BL2到SRAM,然后跳转过去。
代码位置在bl1/目录。你打开看看,会发现代码量其实不大,因为功能太受限了。我记得有一次调试,发现BL1死活跳不到BL2,最后定位是栈指针没对齐——就少了个#define宏,折腾了我一整天。
3.3 BL2:承上启下的“大管家”
BL2是第二个阶段,运行在SRAM里。它比BL1自由多了,因为这时候基本的硬件环境已经搭好。BL2的主要工作是加载和验证后续阶段的镜像。
具体来说:
- 它要读取Flash里的BL31、BL32(如果有)、BL33镜像。
- 验证每个镜像的签名(安全启动流程)。
- 把它们放到正确的位置(内存地址)。
- 最后跳转到BL31。
代码在bl2/目录。你会发现里面有个bl2_main.c,这是入口函数。我建议你从这里开始读代码,逻辑很清晰。
嗯,这里要注意一点。BL2的代码大小是有限制的,因为它也跑在SRAM里。SRAM通常不大,比如256KB或512KB。所以BL2不能太臃肿。我在项目中就遇到过BL2编译后超了SRAM大小,最后不得不裁剪了一些调试日志。
3.4 BL31:安全世界的“操作系统”
BL31是ATF的核心,它实现了ARM的运行时安全固件。说白了,它就是一个跑在安全世界的小型操作系统,负责处理安全监控调用(SMC)、电源管理(PSCI)、安全中断等。
BL31运行在DRAM里,所以代码可以大一些。它的主要模块包括:
| 模块 | 功能 |
|---|---|
| PSCI | 电源管理,比如CPU开关、挂起、唤醒 |
| SMC处理 | 处理来自非安全世界的安全调用 |
| 中断管理 | 安全中断的注册和分发 |
| 上下文管理 | 保存和恢复CPU上下文,用于世界切换 |
代码在bl31/目录。我个人觉得BL31是ATF里最复杂的一块,因为它涉及安全世界和非安全世界的切换。你想想看,每次普通世界调用SMC指令,CPU都要切到安全世界,BL31得保存好普通世界的状态,处理完再切回去。这个过程稍有不慎,系统就崩了。
3.5 BL32:可选的“安全操作系统”
BL32是一个可选的阶段。它运行一个真正的安全操作系统,比如OP-TEE(开源安全扩展)。如果你不需要TEE(可信执行环境),可以完全跳过BL32,直接从BL31跳到BL33。
BL32的作用是提供更丰富的安全服务,比如:
- 安全存储(密钥、证书)
- 加密运算加速
- 可信用户界面
代码在bl32/目录。但说实话,这个目录里通常只放一些与ATF交互的粘合代码,真正的TEE操作系统源码在另一个仓库里。ATF这边只需要知道怎么加载和跳转到BL32就行。
我曾经在一个项目里集成OP-TEE,折腾了两周才跑通。最大的坑是内存布局——BL32需要一块安全DRAM区域,这块区域不能被普通世界访问。如果你在设备树里没配好,BL32一启动就挂。
3.6 BL33:最后的“跳板”
BL33就是最终的非安全世界引导程序。在大多数平台上,它就是U-Boot或者UEFI。ATF的任务就是把控制权交给BL33,之后就不再干预了。
BL33的加载由BL2完成,跳转由BL31完成。流程是这样的:
- BL2把BL33镜像加载到指定内存地址。
- BL31完成安全世界初始化后,执行一条ERET指令,切换到非安全世界。
- CPU开始执行BL33代码,也就是U-Boot开始跑。
代码不在ATF仓库里,BL33是外部项目。ATF这边只需要在平台代码里定义BL33的加载地址和入口地址就行。
3.7 各阶段的关系图
说了这么多,我画张图帮你理一理。这张SVG图展示了从BL1到BL33的完整启动流程:
从这张图你能看到,整个启动过程是链式的。每个阶段只做自己分内的事,然后交给下一个阶段。这种设计的好处是职责单一、易于验证。坏处嘛,就是启动时间会稍微长一点——每个阶段都要做初始化、验证、加载。
3.8 各阶段的内存布局
最后,我列个表总结一下各阶段的内存位置和特点。这对你移植时配置内存非常有用:
| 阶段 | 运行位置 | 是否可修改 | 主要职责 |
|---|---|---|---|
| BL1 | ROM(或片上SRAM) | 否(出厂固化) | 最小硬件初始化,加载BL2 |
| BL2 | SRAM | 是(平台代码) | 加载并验证BL31/BL32/BL33 |
| BL31 | DRAM(安全区域) | 是(平台代码) | 安全监控、电源管理、SMC处理 |
| BL32 | DRAM(安全区域) | 是(可选) | 运行安全操作系统 |
| BL33 | DRAM(非安全区域) | 是(外部项目) | 引导最终操作系统 |
这里有个关键点你要记住:BL1和BL2跑在SRAM里,因为这时候DRAM还没初始化。BL31、BL32、BL33都跑在DRAM里,但安全和非安全区域要严格隔离。我在项目中见过最典型的错误,就是BL31和BL33的内存区域重叠了,结果BL31一跑就把BL33的代码踩了,系统直接死机。