2. 启动参数结构:内核参数、initrd与dtb传递机制、UEFI与BIOS差异
启动参数这事儿,说白了就是操作系统在“睁眼”之前,你塞给它的最后一张小纸条。纸条写对了,系统一路绿灯;写错了,直接黑屏或者 kernel panic。我这些年排查过的启动故障,少说有一半都能追溯到参数传递环节。
这一节我们就把启动参数的骨架拆开看。你搞懂了内核参数、initrd、dtb 是怎么传进去的,以及 UEFI 和 BIOS 在这件事上到底有什么不同,后面遇到问题才能快速定位。
2.1 内核参数:console、root、init 三剑客
内核参数是启动时传给 Linux 内核的字符串,格式通常是 key=value 或者单纯的 flag。我习惯把它们分成三类:控制台、根文件系统、初始化程序。
| 参数 | 作用 | 典型值 | 我踩过的坑 |
|---|---|---|---|
console |
指定内核日志输出到哪里 | ttyS0,115200 / tty0 |
串口线没接,console 配了也白配 |
root |
指定根文件系统设备 | /dev/sda1 / UUID=xxx |
磁盘顺序变了,root 指向了空盘 |
init |
指定第一个用户态进程 | /sbin/init / /bin/bash |
init 路径写错,系统直接挂起 |
console 参数的一个细节:如果你同时指定了 console=tty0 console=ttyS0,115200,内核会把日志同时输出到两个设备。但最后一个 console 参数会成为 /dev/console 的默认目标。我遇到过有人把顺序搞反,结果串口日志全无,还以为硬件坏了。
举个例子,一个典型的 x86 服务器启动参数长这样:
console=tty0 console=ttyS0,115200n8 root=UUID=3a6b8c9d-... ro quiet
这里 ro 表示以只读方式挂载根文件系统,quiet 是减少内核日志输出。嗯,调试阶段我一般会去掉 quiet,甚至加上 earlyprintk 来捕获早期崩溃信息。
2.2 initrd 与 dtb:内核的“助跑器”和“地图”
initrd(Initial RAM Disk)是一个临时根文件系统,加载到内存里让内核先跑起来。为什么需要它?因为内核自己不认识磁盘文件系统,得靠 initrd 里的驱动去加载真正的根分区。
dtb(Device Tree Blob)则是嵌入式领域的“硬件地图”。ARM 或 RISC-V 平台不像 x86 那样能自动枚举设备,必须靠 dtb 告诉内核:CPU 有几个核、内存地址范围、外设挂在哪里。
我个人习惯:在调试阶段,我会把 initrd 里的 /init 脚本改一改,加几行 echo 输出。这样能确认 initrd 是否被正确加载,以及内核参数是否传到了用户态。
传递机制其实不复杂。以 UEFI 系统为例:
- Bootloader(如 GRUB)读取配置文件,拼接出完整的命令行字符串。
- Bootloader 把 initrd 文件加载到内存,把它的起始地址和大小传给内核。
- 对于 dtb,要么由 Bootloader 从磁盘加载,要么由 UEFI 固件提供(通过
EFI_DTB_TABLE)。 - 内核启动时,解析命令行,挂载 initrd,执行
/init。
我曾经遇到过一个案例:某 ARM 服务器换了新网卡,但内核始终认不出来。折腾了半天,发现是 dtb 文件没更新,固件还在用旧的设备树。新网卡的寄存器地址根本不在 dtb 里,内核自然找不到它。
2.3 UEFI 与 BIOS 启动参数差异
BIOS 和 UEFI 在启动参数传递上,思路完全不同。我整理了一张对比表,你一看就明白:
| 对比项 | BIOS (Legacy) | UEFI |
|---|---|---|
| 参数传递方式 | 通过 bootloader(如 GRUB Legacy)拼接字符串 | 通过 EFI_LOADED_IMAGE_PROTOCOL 传递命令行 |
| initrd 加载 | bootloader 直接读取磁盘扇区 | bootloader 调用 UEFI 文件系统协议读取 |
| dtb 来源 | 通常由 bootloader 从固定分区加载 | 可由 UEFI 固件提供,也可由 bootloader 覆盖 |
| 参数长度限制 | 通常 256 字节以内(GRUB Legacy 限制) | 可达 4096 字节甚至更长 |
| 调试难度 | 参数写错只能看屏幕或串口 | 可通过 UEFI Shell 或 efibootmgr 查看 |
注意:UEFI 下如果使用 efibootmgr 设置启动参数,参数是存储在 NVRAM 里的。如果 NVRAM 损坏或空间不足,启动参数可能丢失或截断。我见过一台机器因为 NVRAM 满了,导致内核参数被截断,根文件系统挂载失败。
UEFI 还有一个好处:它支持 EFI_STUB 内核。也就是说,内核本身可以作为一个 UEFI 应用程序直接启动,不需要 GRUB 这类 bootloader。这时候启动参数通过 UEFI 的 LoadOptions 传递。我在调试嵌入式 UEFI 设备时经常用这个特性,少了一层 bootloader,问题定位更直接。
2.4 参数传递流程图
下面这张图展示了从 Bootloader 到内核的参数传递全链路。我建议你把它存下来,排查问题时对着看,思路会清晰很多。
2.5 实战:如何验证参数是否传对
排查启动参数问题,我一般按这个顺序来:
- 看内核日志:启动后执行
dmesg | grep "Command line",确认内核实际收到的参数。如果跟你配置的不一致,说明 bootloader 或固件做了手脚。 - 检查 /proc/cmdline:这个文件就是内核最终使用的命令行。我见过有人改了 GRUB 配置但忘了更新引导记录,结果 /proc/cmdline 还是旧的。
- UEFI 下用 efibootmgr:
efibootmgr -v可以查看当前启动项的完整参数。如果参数里有乱码或截断,基本就是 NVRAM 的问题。 - BIOS 下看 GRUB 菜单:启动时按
e键编辑启动项,确认参数显示完整。我曾经遇到过 GRUB 配置文件里参数写对了,但菜单显示时被换行符截断,导致后半段参数丢失。
一个小技巧:如果你不确定 dtb 是否被正确加载,可以在内核参数里加上 dtb_debug(某些内核支持),或者直接 cat /sys/firmware/devicetree/base/model 查看设备树里的 model 字段。如果文件不存在或内容不对,说明 dtb 没传进来。
嗯,这一节的内容就到这里。参数传递看起来是小事,但一旦出问题,排查起来往往很绕。你把这些结构理清了,后面遇到“内核没输出”、“根文件系统挂不上”、“设备认不出”这类故障,就能快速判断是参数的问题,还是硬件的问题。