2、BootROM与Bootloader:芯片级启动流程,Bootloader的加载、校验与职责。

好,我们接着往下聊。上一章我们把整个Android启动的宏观流程捋了一遍,从按下电源键到Launcher出现,心里大概有个谱了。这一章,咱们得把镜头拉到最底层,看看芯片刚上电那会儿,到底发生了什么。

说白了,就是BootROM和Bootloader的故事。这部分内容,我当年刚入行时也觉得挺神秘的,感觉像是硬件和软件之间的“灰色地带”。后来自己动手调过几次启动,踩过几个坑,才真正搞明白。嗯,咱们今天就把它彻底说透。

2.1 上电瞬间:CPU在干什么?

你按下电源键,电压稳了,时钟信号有了。这时候,CPU的复位逻辑被触发,它做的第一件事是什么?

不是加载Linux内核,也不是启动Android。它连文件系统是什么都不知道。

CPU做的第一件事,是去一个固定的物理地址读取第一条指令。这个地址,是芯片设计时硬编码死的,谁也改不了。比如,高通平台通常是0x00400000,MTK平台可能是0x00000000。这个地址里存的是什么?就是BootROM。

核心概念:BootROM是固化在芯片内部的一块只读存储器(ROM),出厂就写死了。它包含了芯片上电后最先执行的、最基础的启动代码。

我个人习惯把BootROM看作是芯片的“本能”。它不需要任何外部条件,只要芯片有电,它就能跑。它的代码量极小,通常只有几十KB,但功能极其关键。

2.2 BootROM的三大核心职责

BootROM虽然小,但活儿可不轻。我总结了一下,它主要干三件事:

  1. 最基本的硬件初始化:关看门狗、初始化系统时钟、设置堆栈指针。这些操作是后续所有代码运行的基础。你想想看,如果堆栈指针都没设好,代码一调用函数就崩了。
  2. 确定启动介质:BootROM会去检测外部引脚的电平状态,或者读取芯片内部的eFuse(一次性可编程存储),来判断应该从哪里加载下一级代码。是从eMMC?UFS?还是NAND Flash?甚至是USB下载模式?
  3. 加载并校验Bootloader:这是最关键的一步。BootROM会从确定的启动介质中,把Bootloader的第一阶段(通常是SPL或PBL)读到芯片内部的SRAM中,然后进行校验。校验通过,才把控制权交给它。

避坑指南:我曾经在一个项目里,因为eFuse配置错误,导致BootROM死活检测不到eMMC,一直卡在USB下载模式。查了两天才发现是产线烧录时eFuse写错了。所以,硬件启动配置的验证,一定要在板子打样回来第一时间就测。

2.3 Bootloader的加载与校验:信任链的起点

BootROM把Bootloader加载到SRAM后,不会直接跳过去执行。它要先做一件事——校验。

为什么要校验?因为Bootloader是外部存储介质上的代码,是可以被篡改的。如果Bootloader被植入了恶意代码,那整个系统就完了。所以,从BootROM开始,就要建立一条“信任链”。

校验的方式,通常是数字签名验证。BootROM内部固化了公钥,它会用这个公钥去解密Bootloader的签名,然后比对哈希值。只有签名验证通过,BootROM才会认为这个Bootloader是可信的。

// 伪代码示意:BootROM中的校验逻辑
if (verify_signature(bootloader_addr, bootloader_size, public_key) == PASS) {
    jump_to(bootloader_addr); // 校验通过,跳转执行
} else {
    enter_download_mode();     // 校验失败,进入下载模式等待刷机
}

这个机制,就是Android Verified Boot(AVB)的底层基础。没有这一步,上层所有的安全策略都是空中楼阁。

2.4 Bootloader的职责:承上启下

Bootloader拿到控制权后,它的角色就变了。它不再是那个被BootROM“审视”的代码,而是整个系统的“大管家”。它的职责,我归纳为以下几点:

职责 说明 我的经验
初始化DRAM BootROM阶段只能用SRAM,容量极小。Bootloader需要初始化DRAM控制器,让大内存可用。 DRAM初始化参数调不好,系统会随机死机。我见过最离谱的一次,是参数里一个时序值写错了,导致手机在低温下频繁重启。
加载内核与dtb 从存储介质中读取Linux内核镜像(zImage/Image)和设备树(dtb)到内存中。 这里要注意分区表的解析。不同厂商的分区表格式不一样,写代码时一定要兼容处理。
提供fastboot功能 允许用户通过USB与设备通信,进行刷机、解锁等操作。 fastboot是调试利器。我习惯在Bootloader里加一个“进入fastboot”的物理按键检测,方便开发阶段随时打断启动流程。
传递启动参数 将硬件信息、启动模式等参数通过tag或设备树传递给内核。 参数传递错误,内核可能无法识别某些硬件。比如,如果没正确传递“console=ttyS0”,你就永远看不到内核的启动日志。

2.5 两级Bootloader:SPL与U-Boot

你可能会问,Bootloader本身是不是也分阶段?没错,现代SoC通常采用两级Bootloader设计。

  • 第一级:SPL (Secondary Program Loader) 或 PBL (Primary Boot Loader)。它由BootROM加载到SRAM中执行,代码量极小,主要任务就是初始化DRAM,然后从存储介质中加载第二级Bootloader到DRAM中。
  • 第二级:通常是U-Boot或Little Kernel (LK)。这才是我们常说的“完整版”Bootloader。它功能丰富,有命令行、文件系统支持、网络支持等。它负责加载内核、启动Android。

为什么这么设计?说白了,就是SRAM太贵了,容量也小。BootROM那点空间,放不下一个功能完整的Bootloader。所以,先搞个“小不点”SPL,把DRAM初始化好,再把“大块头”U-Boot请进来。

注意:SPL本身也是需要校验的。它的校验由BootROM完成。而U-Boot的校验,则由SPL完成。这样一级一级校验下去,就形成了一条完整的信任链。任何一环被破坏,系统都无法启动。

2.6 一个典型的启动时序图

为了让你更直观地理解,我画了个简单的时序图(用文字描述):

上电 -> CPU复位 -> 读取BootROM -> 执行BootROM代码
    |
    +---> 初始化时钟、堆栈
    +---> 检测启动引脚 -> 确定从eMMC启动
    +---> 从eMMC的特定分区(如boot0)读取SPL到SRAM
    +---> 校验SPL签名 -> 通过
    +---> 跳转到SPL执行
            |
            +---> 初始化DRAM
            +---> 从eMMC的boot1分区读取U-Boot到DRAM
            +---> 校验U-Boot签名 -> 通过
            +---> 跳转到U-Boot执行
                    |
                    +---> 初始化更多硬件(如USB、网络)
                    +---> 读取启动参数(如bootargs)
                    +---> 从boot分区读取Linux内核到DRAM
                    +---> 校验内核签名 -> 通过
                    +---> 跳转到内核执行
                            |
                            +---> 内核启动 -> init进程 -> Zygote -> SystemServer -> Launcher

你看,整个流程环环相扣,每一级都在为下一级做准备,同时也在校验下一级的合法性。这就是Android系统启动的“信任链”模型。

2.7 小结与思考

这一章,我们深入到了芯片内部,看了BootROM和Bootloader是怎么工作的。我个人觉得,理解这部分内容,对于排查启动问题、做底层安全开发,都特别有帮助。

最后,留个思考题给你:如果BootROM的代码有bug,怎么办?它可是固化在芯片里的,没法OTA升级。嗯,这个问题,我们下一章聊系统分区的时候,会涉及到。

公众号:蓝海资料掘金营,微信deep3321