ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

嵌入式Linux启动全流程拆解:从MaskRom到Loader再到内核

嵌入式Linux启动全流程拆解:从MaskRom到Loader再到内核

你有没有仔细想过,一块开发板从按下电源键到屏幕上出现登录提示符,中间到底经历了什么?如果把这趟旅程叫做破晓之路,那它的起点其实并不是Linux内核,而是芯片上电后执行的第一段程序所在的MaskRom,然后经由Loader,最后才轮到Linux内核登场。对很多只接触过应用开发的人来说,这三个名字几乎是一个黑盒,启动失败时只能盲目重刷固件,很难定位问题。

这篇文章我想从底层视角把这条启动链完整拆开。我会说明每一棒的责任边界、交接方式,以及实践中最常见的坑。不管你是嵌入式新手、BSP工程师,还是对固件安全感兴趣的研究者,都能从这些内容里找到一个可对照、可操作的框架。

1. 启动流程全景:一次设计精巧的多级接力

1.1 上电瞬间从第一行代码开始

电源稳定、复位释放之后,CPU要做的第一件事就是取指执行。问题是:这时候Flash、DDR、外设控制器都还没初始化,CPU根本不可能从外部存储里拿代码。于是SoC设计者把一小段启动代码直接蚀刻在芯片内部的只读存储器里,通常叫做MaskRom。芯片会把这个只读存储区映射到固定的复位地址,CPU上电后自动跳转过去,这段代码就是整条启动链的起点。

这一步往往被很多人忽略,但它其实决定了整条链路的可靠性。MaskRom代码只在晶圆制造时写入一次,以后谁也无法修改。因为它足够小、足够固定,才能成为后续所有信任的基础。你可以把它理解成一栋楼的地基:上面的装修再好,地基只要被污染,整栋楼都失去意义。

1.2 为什么设计成接力跑而不是一步到位

为什么不干脆让BootROM就把内核加载起来?原因很现实。第一,BootROM空间极其有限,通常只有几十KB,而一个完整内核镜像少说也有几兆字节,代码根本塞不进去。第二,内核启动的前置条件很复杂,需要先初始化DDR内存控制器、文件系统、显示系统等,这个工程量不是一个小ROM能够承担的。第三,考虑到安全和灵活性,把系统拆成多级,每一级做有限的校验和初始化,既可以逐级验证,也便于后续替换升级。

这个设计和生活中的快递中转站很像:一辆货车没有必要从发货地一路开到每一户门口。把任务拆成干线运输和末端配送,由不同角色接力完成,效率更高,也更容易被管控。启动链中的MaskRom、Loader、内核,本质上就是干线、中转站、末端配送的关系。

2. 第一棒:MaskRom阶段的底层真相

2.1 MaskRom是什么,为什么它不可篡改

MaskRom的全称是掩膜只读存储器,简单说就是半导体工厂在制造芯片时,用掩膜版把数据直接“刻”进硅片的存储单元里。它不是Flash那种可擦写的设备,内容出厂后固定不变,无法通过任何软件手段改写。很多SoC把第一级启动代码放在这种存储里,因此它常直接代表芯片内部的BootROM程序。

把这个设计放在安全视角来看,意思是不管外部固件再怎么烂,芯片里总有一段“永远不会被污染”的代码作为起点。这段代码通常只做最基础的事情:设置栈指针、初始化一部分时钟、检测启动引脚,以及尝试从若干外部介质中搬取一小段数据。因为存储空间紧张,它并不会帮你做复杂的文件系统解析,也不会有完整的外设驱动。

2.2 MaskRom启动后按什么顺序工作

不同SoC的实现细节各不相同,但流程大体一致。CPU上电后,MaskRom先做一些异常向量表和栈的准备,避免后续代码跑飞;接着初始化必要的时钟和仲裁器,使CPU能访问片上SRAM;然后,通过启动引脚上的电平组合判断本次从哪个介质引导,比如SD卡、eMMC、SPI Nor,或者进入下载模式;确定介质后,直接在物理地址上读取固定偏移处的数据块到SRAM;数据内容会先判断合法性,比如看头部的魔数和校验值,有时还包括签名,通过后才跳过去执行。

这一步的逻辑常常表现为启动速度的差异。比如你的板子上电后需要几百毫秒才看到串口打印,很大部分时间就花在了MaskRom对介质的尝试轮询上。如果介质中没有合法数据,它会挨个尝试下一个介质,直到所有选项耗尽,可能才会进入一个等待下载的兜底模式。

2.3 下载模式:真正的救砖通道

MaskRom除了正常引导,还会提供一条后门式的下载通道。通常由一组硬件引脚组合触发,比如按住一个按键,或者短接测试点再上电。此时MaskRom不会去读启动介质,而是初始化USB或串口设备,等待上位机固件工具连接。在这个模式下,即使开发板里完全没有Loader、内核和根文件系统,依然能和工具建立连接,写入初始Loader。

很多所谓的变砖,并不是物理损坏,只是启动介质里的Loader被写坏。只要MaskRom还在,设备就等于还有一种最低限度的生命体征。我自己的经验是:遇到刷机失败导致设备无任何显示、串口也没有反应时,不要急着认定报废,先找到板子的恢复按键或MaskRom触点,按住后重新上电,观察电脑设备管理器是否出现一个未知下载设备。只要识别到,就还有救。

3. 第二棒:Loader——把内核扶上马的关键角色

3.1 为什么BootROM之后还需要一个Loader

MaskRom已经把代码搬运到了SRAM,但对Linux内核来说,这还远远不够。内核通常以压缩镜像存在,需要解压到几MB甚至几十MB的内存中运行,而BootROM连DDR本身都不会初始化,更别说解压文件系统。所以中间需要Loader这样的全能中转站,来补齐所有准备工作。

Loader的对外称呼很多,比如Bootloader、引导程序、BIOS。在嵌入式Linux世界里,最常见的就是开源社区维护的通用引导程序,配合各SoC厂商的二阶段组件。它负责初始化DDR、时钟、存储控制器,把内核映像、设备树、初始内存盘从Flash或磁盘中取出并加载到内存中,做好交接前的所有布置。

3.2 为什么Loader经常要拆成两段来跑

这里有一个很实际的问题:BootROM能加载到SRAM的数据量是有限的。SRAM通常只有几十到几百KB,而完整Loader的功能组件可能要按MB计算。要解决这组矛盾,业界标准做法是把Loader模块化:先有一个极小的第一段加载器,在SRAM里运行,它的任务只有一个,就是把DDR内存控制器初始化好;然后由这段小程序把完整的Loader主体从介质拷入DDR,再跳过去跑完整主体。

第一段加载器通常叫SPL或者TPL,完整主体则叫主Loader。很多启动日志里能看到早在主Loader打印之前,就已经有了一些简短的输出,那就是第一段加载器在做事。如果你在调试DDR参数,那第一段加载器就是最先受影响的代码,也是最容易让整个启动链静默失败的地方。

3.3 Loader怎么选择启动介质和分区

进入完整Loader阶段后,代码开始具备比较完整的设备驱动,可以读取SD卡、eMMC、NAND、NVMe等。Loader通常会维护一组启动设备优先级:先从可移动设备启动,然后从内部eMMC启动,再从网络启动。这个顺序可以通过配置或者命令行环境变量改变。

读取介质时,Loader有两种常见策略。一种是直接按物理偏移读取,比如内核放在第x个扇区开始的位置,Loader绕过文件系统直接读裸扇区;另一种是识别文件系统,比如在FAT或ext4分区里查找特定文件名的内核镜像。前者实现简单、可靠,但不能像日常文件管理那样随意改名;后者的使用体验比较好,但对文件系统代码的完整性要求更高。选择哪一种,会影响后续更新固件的便利程度。

3.4 交接仪式:Loader给内核传递了什么

当Loader已经准备好一切,它把内核镜像放到一个约定的内存地址,把设备树二进制(DTB)加载到另一块指定内存,然后把两个地址塞到约定的寄存器里。接着设置CPU的工作模式,关闭或打开对应的cache和MMU选项,最终跳转到内核入口地址。跳转前的这组寄存器约定,就是所谓的内核启动ABI,直接决定了内核能否顺利进入后续阶段。

很多新手在这里搞错的是内存地址。比如内核镜像编译时用的是虚拟地址空间,但Loader跳转之前必须把它加载到内核映射到的物理地址区域。如果放错位置,内核第一条指令就会执行到无效区域,结果往往是没有日志,或者复位重新开始。这也是为什么每次更换内核或调整内存布局后,都要回头确认Loader里的加载地址没有漂移。

3.5 安全启动链:从BootROM到内核的信任接力

现代SoC普遍加入了安全启动机制。MaskRom芯片内部藏有公钥哈希,通常熔断在eFuse里。BootROM拿到Loader后,先对Loader的签名做校验,签名通过才允许执行。Loader得到控制权之后,再对内核和设备树做同样的事情。整条链路上每一层都只信任上一层的校验结果,这就是信任链。

这个机制对调试的影响很大。如果你开启了安全启动,自定义编译的Loader或者内核没有经过正确的签名工具处理,就会被拒绝执行,现象是卡在某一步没有后续输出,甚至反复复位。遇到这种情况,先不要怀疑硬件,检查是否使用了正确签名的固件,以及是否关闭了强制安全启动。生产环境下安全启动很有价值,但开发阶段如果没配置好,会让人浪费大量时间。

4. 第三棒:Linux内核的破晓时刻

4.1 内核入口:从压缩镜像到真实代码

Loader把控制权移交的位置,实际上是内核的启动桩。嵌入式Linux内核往往以压缩方式存储,比如自解压镜像。跳转后首先运行的是自带解压代码,这段代码会把真正的内核镜像释放到指定内存位置,随后进入正式入口。从这里开始,CPU开始运行大部分Linux内核开发者熟悉的启动路径:early setup、页表构建、切换到C入口。

在极早期,内核还不能依赖复杂驱动,只能通过汇编代码准备好基本运行环境。随后start_kernel会开启系统各个子系统:调度器、内存管理、中断、定时器、串口、块设备等。整个过程有数以百计的初始化函数按依赖关系依次执行,任何一个早期子模块出错,日志都会戛然而止。很多人在这个阶段看到一行输出,就以为内核已经起来了,其实此时离用户空间还很远。

4.2 设备树如何告诉内核我在哪块板子上

现代内核不再针对每块板子硬编码电路信息,而是依赖设备树描述硬件拓扑。Loader把DTB内存地址传给内核,内核通过解析这些节点得知系统有几核CPU、多少内存、串口地址是多少、哪些外设挂在哪条总线上。驱动通过匹配节点中的兼容字符串,和自身驱动的ID表进行对比,命中后执行probe函数。

这个机制让同一个内核能适配多种开发板和产品,只要提供对应的DTB即可。但这也带来一个常见的坑:内核编译得没问题,驱动模块也齐全,可加载了错误设备树后,串口初始化地址不同,系统直接黑屏或没有日志。所以调试启动时,必须先确认Loader实际加载的是哪个DTB、版本是否匹配。设备树和内核版本之间偶尔会存在兼容问题,旧DTB配新内核同样会导致外设无法工作。

4.3 挂载根文件系统:从内核态到用户态的最后一跳

Linux内核启动后半段,有一个非常关键的标志性动作:寻找并挂载根文件系统。如果内核命令行里带了initramfs,它会优先用内存里的压缩ramdisk展开根目录;否则需要指定一个根设备,比如一个支持ext4的MMC分区。系统等待这个设备出现、文件系统驱动识别成功,然后mount到根目录下。

一旦根文件系统挂载完成,内核会去执行其中的init程序,通常是/sbin/init。从这里开始,内核让出了控制权,用户空间进程开始运行,比如init进程、shell、图形界面。启动流程的破晓之路最终以用户态的第一个进程跑起来作为终点。整个从MaskRom到Loader再到Linux内核的链路,到这里才算圆满闭环。

5. 实操调试:抓住每一棒交接点

5.1 串口日志是启动排障的第一手证据

要调试启动问题,串口日志几乎是必备工具。把USB转串口模块连接到开发板的调试串口引脚,通常需要TX、RX交叉连接,并共地。波特率常见为115200,也有芯片用1500000等更高速率,可以在Loader文档里确认。打开任意串口终端软件,配置好8位数据位、无校验、1位停止位,然后给板子上电,就能看到从Loader早期到内核的全部输出。

看日志时,我的习惯是先确认几个界标。第一次出现Loader版本信息,说明第一阶段已经跑起来了;第一次出现磁盘或分区扫描信息,说明完整的Loader已经在DDR里运行;出现内核压缩解压信息,说明Loader交接成功;出现挂载根文件系统的信息,说明内核已经跑到了最后阶段。这样能快速定位问题究竟出在哪一段,而不是眉毛胡子一把抓。

5.2 典型故障速查表

现象可能原因建议排查方向
上电完全无输出串口接错、波特率不对、电源或复位故障检查TX/RX交叉和地线,改用示波器量复位信号
串口输出乱码波特率不匹配或电平不兼容确认串口电平是TTL还是RS232,校准波特率
系统反复重启Loader或内核校验失败,DDR初始化不稳定进入下载模式重刷Loader,确认DDR参数
卡在内核早期无日志设备树不匹配、内存信息错误、内核镜像损坏检查bootargs,换用官方出厂DTB
挂载根文件系统失败root参数错误、分区未格式化、缺少驱动检查cmdline,确认内核对文件系统的支持
内核panic后立刻复位根文件系统不可用或init进程崩溃用initramfs测试,避免直接挂磁盘根fs

这张表不算万能,但覆盖了我在不同板子上遇到过的80%问题。配合串口日志里的界标,基本能把问题缩小到某一棒之后。

5.3 不同阶段掉进过哪些坑

第一类坑在MaskRom与下载模式之间。有的板子下载模式不是按键触发,而是要通过短路特定测试点。短错点可能导致供电异常。在下载设备被识别出来之前,一定要先确认硬件连接有没有问题,不要反复刷写同一个动作。

第二类坑在Loader阶段。比如一个板子上的Loader环境变量被改坏,导致启动介质顺序错乱,直接跳过了本该启动的eMMC。大多数人对着内核日志找了半天,最后才发现是Loader命令行里一个变量写错了。这类问题一般可以通过在Loader停顿阶段进入命令行,用printenv查看环境变量、用saveenv恢复默认值来解决。

第三类坑在启动参数传递。bootargs里的console参数如果指定了不存在的串口,或者root挂载参数与实际分区不对,内核大概率死在启动早期。调试这类问题需要先把console参数简化,比如明确指定为某个实际存在的串口,让内核尽早打印,再逐步增加功能。

5.4 从MaskRom救砖的完整操作思路

假设现在真的变砖了,串口也没有输出。我的建议流程是:先给板子断电,找到主芯片旁边的恢复按键或者MaskRom测试点。把板子通过数据线连到主机,但不进行刷写操作;按住恢复键再上电,持续两到三秒,等待主机端出现一个无驱动的下载设备。如果出现了,说明MaskRom下载通道还活着。

接着使用对应SoC的刷机工具,选择一个包含Loader、启动参数、内核和根文件系统的完整镜像包,按工具提示进入恢复烧录模式。耐心等进度条走完,不要中途断电拔线。完成后再上电,多数设备就能从Loader重新开始正常引导。如果设备完全无响应,再考虑检查硬件电源、晶振和复位电路,但至少软件层面你已经试完了最后一招。

6. 写在最后:启动调试的个人经验谈

6.1 我总结的三条启动调试经验

前前后后调过不少启动问题,我最深的感受是:启动链条看起来很长,但只要在每个交接点上分别做记录,定位问题是非常有规律的。比如先把日志分成MaskRom之前和之后,再分成Loader阶段和内核阶段,自然就能判断问题方向。

第二条经验是:遇到诡异的问题,先怀疑环境变量。很多人在内核、驱动、设备树上排查半天,最后发现不过是Loader的命令行里多了一个不存在的参数,或者启动介质顺序被改乱。所以在所有深度排查之前,先确认Loader是否在用预期配置。

第三条经验是:保留原始备份是所有调试工作的底线。在做出任何刷机、改环境变量、写设备树的操作前,先把当前能正常启动的固件完整备份一份,同时记录Loader环境变量原始值。很多时候你需要的不是高深的调试技巧,而是能回到上一步的能力。

6.2 一个能解决很多疑难杂症的压箱底技巧

最后再分享一个小技巧。如果哪天你手里板子的Loader环境变量被改得乱七八糟,不如彻底清空环境变量区域,让Loader回到默认配置。很多疑难杂症,比如启动介质不对、参数错误、内核反复死机,往往就这样在一个干净环境里自己就暴露了。启动流程虽然很长,但只要思路清楚,破晓之路并不神秘,每一步都可以被校准,也值得被校准。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进