第4章:Bootloader的角色:GRUB、U-Boot等Bootloader的工作原理与配置方法
说到系统启动,Bootloader是个绕不开的角色。它就像系统的「接生婆」——硬件上电后第一个跑起来的软件,负责把操作系统内核请进内存。我这些年调过不少启动问题,从嵌入式板子到服务器,Bootloader出问题时的表现千奇百怪,但归根结底,理解它的工作原理才是解决问题的钥匙。
4.1 Bootloader到底在干什么?
说白了,Bootloader就干三件事:初始化硬件、加载内核、跳转执行。你想想看,CPU上电时内存还没初始化,硬盘驱动也没加载,这时候谁来把内核从磁盘搬到内存里?就是Bootloader。
我个人习惯把Bootloader分成两类:
- PC/服务器领域:GRUB(GRand Unified Bootloader)是绝对主流
- 嵌入式领域:U-Boot(Universal Bootloader)几乎一统天下
它们虽然应用场景不同,但核心逻辑是相通的。嗯,这里要注意:Bootloader本身也分阶段执行,这是为了克服硬件限制——早期代码必须放在很小的ROM里,所以先跑一小段初始化代码,再加载完整的Bootloader主体。
核心概念:Bootloader的两阶段设计
第一阶段(Stage 1):放在ROM或MBR里,代码极小,负责初始化基本硬件并加载第二阶段。
第二阶段(Stage 2):完整的Bootloader程序,提供菜单、文件系统支持、内核加载等功能。
4.2 GRUB:PC世界的启动管家
GRUB是Linux世界里用得最多的Bootloader。我最早接触GRUB是在2005年配双系统的时候,那时候还是GRUB Legacy。现在主流是GRUB 2,配置方式变化很大。
4.2.1 GRUB的工作流程
GRUB 2的启动过程分三步:
- boot.img:放在MBR(主引导记录)里,只有446字节。它的任务就是找到core.img的位置。
- core.img:包含文件系统驱动,能识别/boot分区,然后加载完整的GRUB模块。
- grub.cfg:真正的配置文件,显示菜单,加载内核。
我曾经遇到过一个问题:磁盘从MBR改成GPT分区后,GRUB死活装不上。后来才发现,GPT需要专门的BIOS boot分区来放core.img。这个坑让我折腾了大半天。
4.2.2 GRUB的配置方法
GRUB 2的主配置文件是 /boot/grub/grub.cfg。但注意——不要直接编辑这个文件!它是由 grub-mkconfig 自动生成的。正确的做法是修改 /etc/default/grub,然后运行:
# 更新GRUB配置(Debian/Ubuntu系)
sudo update-grub
# 或者(RHEL/CentOS系)
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
来看看 /etc/default/grub 里常用的配置项:
| 配置项 | 说明 | 示例值 |
|---|---|---|
| GRUB_DEFAULT | 默认启动项(数字或菜单名) | 0 或 "Ubuntu" |
| GRUB_TIMEOUT | 菜单等待秒数(-1表示无限等待) | 5 |
| GRUB_CMDLINE_LINUX | 传递给内核的启动参数 | "quiet splash" |
| GRUB_GFXMODE | 图形菜单分辨率 | "1024x768x32" |
| GRUB_DISABLE_RECOVERY | 是否禁用恢复模式菜单项 | true |
个人经验:调试内核启动问题时,我习惯在GRUB_CMDLINE_LINUX里加上 earlyprintk 和 ignore_loglevel。这样内核在早期初始化阶段的打印信息就不会被过滤掉,定位问题快很多。
4.2.3 手动编辑GRUB菜单项
有时候自动生成的配置不够灵活,比如你想临时加一个内核参数调试。可以在启动时按 e 键编辑当前菜单项,找到 linux 开头的行,在末尾加上参数。这个技巧我在现场排查问题时用过无数次。
举个例子,假设你想让内核以单用户模式启动:
linux /vmlinuz-5.15.0-generic root=/dev/sda1 ro single
按 Ctrl+X 或 F10 启动即可。注意这只是临时修改,重启后恢复原样。
4.3 U-Boot:嵌入式世界的标准答案
转到嵌入式领域,U-Boot几乎是事实标准。从ARM到RISC-V,从单片机到多核处理器,U-Boot都能搞定。我最早在PowerPC平台上用U-Boot,那时候它还叫PPCBoot,后来才改名。
4.3.1 U-Boot的启动流程
U-Boot也是分阶段设计:
- SPL(Secondary Program Loader):相当于GRUB的Stage 1,放在片内SRAM或ROM里,初始化DDR内存,然后加载完整的U-Boot。
- U-Boot proper:完整的Bootloader,提供命令行、网络、文件系统支持。
为什么会这样设计?因为很多嵌入式芯片的片内RAM只有几十KB,放不下完整的U-Boot。SPL就是那个「瘦身版」的加载器。
4.3.2 U-Boot的环境变量
U-Boot的配置核心是环境变量,存储在Flash或EEPROM里。常用的环境变量有:
# 查看所有环境变量
printenv
# 设置环境变量
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rw'
setenv bootcmd 'mmc dev 0; ext4load mmc 0:1 0x80000000 /boot/zImage; bootz 0x80000000'
# 保存到存储设备
saveenv
我个人习惯把常用的启动命令写成脚本存到环境变量里。比如:
setenv myboot 'tftp 0x80000000 zImage; tftp 0x83000000 dtb; bootz 0x80000000 - 0x83000000'
saveenv
这样每次调试时只需要输入 run myboot 就能从网络启动,省去重复输入命令的麻烦。
避坑指南:我曾经在量产阶段遇到过U-Boot环境变量损坏导致设备变砖的情况。原因是Flash的坏块管理没处理好。从那以后,我坚持在环境变量区使用冗余备份,并且每次saveenv后都做一次校验。
4.3.3 U-Boot的配置与编译
U-Boot使用Kconfig配置系统(和Linux内核一样)。配置方法:
# 以某ARM开发板为例
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- orangepi_zero_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
生成的 u-boot.bin 就是最终的Bootloader镜像。注意有些平台还需要额外的打包步骤,比如生成 u-boot-spl.bin 和 u-boot.img。
4.4 Bootloader的通用调试技巧
不管用GRUB还是U-Boot,有些调试思路是通用的:
- 串口输出是王道:嵌入式板子一定要引出串口,很多启动问题看串口日志就能定位。
- 从简单到复杂:先确认Bootloader能跑起来,再调试内核加载,最后才看文件系统。
- 保留一个可靠的启动方式:比如SD卡启动或网络启动,作为「救急通道」。
我记得有一次帮客户定位一个启动问题,现象是内核加载到一半就卡死。我让客户把串口日志发过来,发现是U-Boot传给内核的设备树(DTB)地址不对。调整了 fdt_addr 环境变量后,问题就解决了。你看,有时候就是一个小参数的事。
核心总结:Bootloader的本质是「硬件初始化 + 内核加载器」。GRUB适合PC/服务器,U-Boot统治嵌入式。理解它们的分阶段设计思想,掌握环境变量和配置文件的用法,就能应对绝大多数启动问题。