2. ATF基础架构
说实话,ATF(Arm Trusted Firmware)这玩意儿,我第一次接触的时候也觉得头大。源码结构复杂,启动流程绕来绕去,还有那个SPD框架,光看文档根本理不清。但做安全架构这么多年,我慢慢摸出了一些门道。今天咱们就把它拆开揉碎了讲。
ATF源码结构:别被目录吓到
ATF的源码,说白了就是一套固件集合。你下载下来,会发现几个核心目录:
- bl1/ — 第一级启动加载器,芯片上电后最先跑的东西
- bl2/ — 第二级启动加载器,负责加载后续镜像
- bl31/ — 安全监控器,这是ATF的核心,处理安全世界和非安全世界的切换
- bl32/ — 安全世界固件,通常是TEE(如OP-TEE)
- bl33/ — 非安全世界固件,一般是U-Boot或EDK2
嗯,这里要注意,bl32 和 bl33 其实不是ATF自己写的,它们是第三方固件。ATF只是负责把它们加载起来,然后跳转过去执行。
核心理解:ATF本质上是一个"固件调度器",它不干具体业务,只负责安全启动和世界切换。
我个人习惯,拿到一个新平台,先看 plat/ 目录下的板级支持代码。每个芯片厂商都会在这里放自己的适配代码。比如 plat/mediatek/、plat/rockchip/ 这些。
BL1/BL2/BL31/BL32/BL33 启动流程
启动流程,说白了就是"谁先跑,谁后跑,谁把谁叫起来"。我画了一张图,帮你理清这个链条:
你看这个图,启动顺序是从左到右的。BL1跑完叫BL2,BL2跑完叫BL31,然后BL31决定要不要叫BL32(TEE),最后跳转到BL33。
我在项目中遇到过一个问题:BL2加载BL31的时候,地址算错了,结果BL31跑飞了。排查了半天才发现是链接脚本里某个宏定义没对齐。所以啊,启动流程里最坑的就是地址对齐问题。
避坑指南:我曾经在调试时发现BL31起不来,最后定位到是BL2传给BL31的参数结构体里,某个字段的偏移算错了。建议你在写板级代码时,多用 assert 检查结构体大小。
各阶段干了什么?
咱们一个一个说:
BL1 — 芯片上电的第一行代码
BL1通常放在ROM里,不可修改。它只做三件事:
- 初始化CPU和最基本的外设(比如UART,方便你打log)
- 从Flash里找到BL2,验证它的签名
- 跳转到BL2
说白了,BL1就是个"信任根"。它验证了BL2,后面的链条就都可信了。
BL2 — 真正的加载器
BL2是存在Flash里的,可以更新。它的任务就是:
- 从Flash里把BL31、BL32、BL33都读出来
- 验证它们的签名(如果开启了安全启动)
- 把它们放到内存里正确的位置
- 跳转到BL31
你想想看,BL2其实是个"快递员",它把各个镜像送到该去的地方。
BL31 — ATF的核心
BL31就是安全监控器(Secure Monitor)。它初始化后,会做两件大事:
- 建立异常向量表,处理安全世界和非安全世界的切换
- 初始化SPD(安全调度器),把BL32(TEE)叫起来
嗯,这里要注意,BL31一旦跑起来,就常驻内存了。它就像一个"看门人",所有从非安全世界到安全世界的请求,都得经过它。
BL32 — TEE操作系统
BL32就是你的TEE,比如OP-TEE、Trusty这些。它运行在安全世界,提供安全服务。BL31通过SPD和它通信。
BL33 — 非安全世界的引导
BL33通常是U-Boot或者EDK2,它负责启动你的Linux或Android系统。BL33运行在非安全世界,它想调用安全服务时,得通过BL31。
SPD框架:ATF和TEE的桥梁
SPD(Secure Payload Dispatcher)是ATF里最巧妙的设计之一。说白了,它就是个"适配器",让ATF能和不同的TEE对接。
为什么需要SPD?因为不同的TEE有不同的接口规范。OP-TEE用SMC调用,Trusty有自己的协议。ATF不可能为每个TEE都写一套代码。所以它定义了一个SPD框架,你只要实现几个回调函数,就能把TEE挂上去。
核心理解:SPD = 标准接口 + 回调函数。ATF定义好框架,TEE厂商填自己的实现。
SPD的核心数据结构是 spd_pm_ops 和 spd_rt_svc_desc。我贴一段简化代码,你看一眼就明白了:
// SPD描述结构体
typedef struct spd_pm_ops {
void (*svc_done)(void); // 服务完成回调
int (*cpu_on)(u_register_t mpidr); // CPU唤醒
void (*cpu_off)(u_register_t mpidr); // CPU休眠
void (*cpu_suspend)(u_register_t mpidr);
void (*cpu_resume)(u_register_t mpidr);
} spd_pm_ops_t;
// 注册SPD
void spd_register(const spd_pm_ops_t *ops);
你看,ATF只关心这几个回调。至于TEE内部怎么处理,ATF不管。这就是"面向接口编程"的思想。
个人经验:我刚开始写SPD适配时,以为要改ATF核心代码。后来发现只需要实现 spd_pm_ops 里的几个函数就行。ATF的框架设计得挺干净的,你不需要动它的核心逻辑。
SPD的启动流程
BL31初始化时,会调用 spd_setup()。这个函数会:
- 找到BL32镜像在内存里的位置
- 初始化TEE的上下文(寄存器状态、栈指针等)
- 调用TEE的入口函数,让TEE跑起来
- 注册SMC处理函数,以后非安全世界调用SMC时,ATF能路由到TEE
我曾经踩过一个坑:SPD初始化时,TEE的栈指针没对齐到16字节,结果TEE一跑就崩。查了两天才发现是栈对齐的问题。所以啊,嵌入式开发里,对齐问题永远是第一位的。
警告:SPD的 cpu_on 回调在多核平台上特别容易出问题。每个核唤醒时,都要重新初始化TEE的上下文。如果你忘了给新核分配独立的栈空间,TEE会在多核场景下随机崩溃。
总结一下
ATF的架构,说白了就是"分层 + 回调"。BL1/BL2负责安全启动,BL31负责安全监控,SPD负责对接TEE。每一层只干自己的事,接口清晰。
你想想看,这种设计的好处是什么?可替换性强。你想换TEE?换个SPD实现就行。你想换启动方式?改BL2就行。ATF的核心代码几乎不用动。
嗯,这就是ATF的设计哲学。下一节咱们会深入SPD的代码实现,手把手教你写一个SPD适配器。
一句话记住:ATF = 安全启动(BL1/BL2) + 安全监控(BL31) + 调度框架(SPD)。
可可精品资料库