3、BL1 (Boot ROM):ATF的第一阶段,固化在ROM中的启动代码
好,咱们正式开始拆解ATF的第一阶段——BL1。
BL1,全称是Boot Loader Stage 1。它被固化在芯片内部的ROM里,芯片一上电,CPU复位后执行的第一条指令,就来自这里。说白了,这是整个系统启动的“原点”。
我个人习惯把BL1叫做“硬启动代码”。为什么?因为它没得改。芯片出厂那一刻,BL1的内容就焊死在硅片上了。你想想看,这玩意儿要是出了bug,那可不是打个补丁就能解决的,得重新流片。所以,BL1的设计哲学就一个字:稳。
3.1 BL1 的核心职责
BL1的任务其实很纯粹,就三件事:
- 初始化最基础的硬件:比如关掉所有中断、设置异常向量表、初始化栈指针。这时候DRAM还没影儿呢,只能用芯片内部的SRAM或者Cache作为临时RAM。
- 验证并加载BL2:BL1从Flash(比如NOR Flash、eMMC、或者SPI Flash)里把BL2的镜像读出来,做完整性校验和签名验证。只有通过验证,才会把控制权交给BL2。
- 跳转到BL2:一切就绪后,BL1会清理自己的痕迹,然后跳转到BL2的入口地址。
关键点:BL1是信任链的起点。它本身是“绝对可信”的,因为它固化在ROM里,物理上无法篡改。后续所有组件的可信度,都建立在BL1对它们的验证之上。
3.2 BL1 的执行环境
嗯,这里要注意。BL1运行在EL3(异常级别3),这是ARMv8架构里权限最高的异常级别。它拥有对系统所有资源的完全控制权。
但有个尴尬的地方:BL1运行的时候,DRAM还没初始化。所以它只能用芯片内部的“小池塘”——也就是SRAM或者Cache。这个空间通常很小,可能只有几十KB到几百KB。我在项目中遇到过,某款芯片的SRAM只有64KB,BL1加上BL2的头部信息,挤得满满当当,稍微多加点调试代码就爆了。
| 资源 | 说明 |
|---|---|
| 异常级别 | EL3 |
| 内存 | 内部SRAM 或 设置为SRAM使用的Cache |
| 存储介质 | Boot ROM(代码本身) + 外部Flash(存放BL2镜像) |
| 栈 | 在SRAM中临时分配,通常很小(几KB) |
3.3 BL1 的典型执行流程
我画了一张流程图,帮你把BL1的整个生命周期串起来。你看完应该就清楚了。
你看,流程很清晰。但有几个细节我想展开聊聊。
3.4 避坑指南:BL1 开发中的常见问题
我曾经在一个项目里,被BL1折腾得够呛。当时芯片的Boot ROM有个隐藏的勘误:在特定条件下,Cache作为RAM使用时,初始化顺序不对会导致数据污染。我们查了整整两周,最后是拿着逻辑分析仪一根一根量信号线才定位到问题。
所以,我给你总结几个实战中容易踩的坑:
- 栈空间溢出:BL1的栈通常只有几KB。如果你在BL1里调用了复杂的函数,或者递归调用,很容易就把栈撑爆了。我建议你在调试阶段,在栈底填充一个固定模式(比如0xDEADBEEF),然后定期检查这个模式有没有被覆盖。
- Flash读取时序:BL1要从Flash里读BL2镜像。但Flash的读取时序(比如SPI的时钟频率、模式)必须和硬件设计匹配。我见过有人把SPI的极性搞反了,结果读出来的数据全是0xFF。
- 签名验证失败:BL1的签名验证是信任链的关键。但如果你把公钥的哈希值存错了位置,或者签名算法参数没对齐,那BL2永远也跑不起来。我曾经犯过一个低级错误:公钥哈希值的大小端搞反了,结果验证死活过不去。
警告:千万不要在BL1里做复杂操作!比如初始化DRAM、加载文件系统、或者打印冗长的调试信息。BL1的空间和时间都极其有限。它的原则是“做完该做的,立刻走人”。任何多余的操作,都可能引入不可预知的风险。
3.5 BL1 与 BL2 的接口约定
BL1跳转到BL2时,不是简单的一条跳转指令就完事了。它们之间有一套约定好的接口规范。在ATF里,这个规范叫做Boot Loader Stage 2 Image (BL2_IMAGE)。
BL1会通过寄存器向BL2传递一些关键信息:
| 寄存器 | 传递内容 | 说明 |
|---|---|---|
| x0 | BL2入口点 | BL2镜像在内存中的起始地址 |
| x1 | 平台信息结构体指针 | 包含内存布局、外设基地址等硬件信息 |
| x2 | BL2镜像大小 | 用于BL2自我校验 |
| x3 | 预留(通常为0) | 用于未来扩展 |
我个人习惯在BL1的最后,把这些寄存器的值打印出来(通过UART),方便调试时确认传递的参数是否正确。你想想看,如果x1传了一个野指针过去,BL2一上来就访问非法地址,那整个系统直接就挂了。
3.6 小结
BL1虽然代码量不大,但它是整个系统启动的基石。它固化在ROM里,不可更改,所以必须经过严格的验证。它的核心任务就是初始化最基础的硬件,验证并加载BL2,然后功成身退。
嗯,说白了,BL1就是个“开门人”。它把门打开,确认进来的人是可信的,然后就把钥匙交给下一个人。至于后面的人要做什么,它不管,也管不了。
小技巧:如果你在调试BL1,建议在UART输出里加上时间戳。BL1的执行时间通常只有几毫秒到几十毫秒。通过时间戳,你可以精确地知道每个步骤花了多少时间,这对优化启动速度非常有帮助。