第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程序,提供菜单、文件系统支持、内核加载等功能。

CPU上电/复位 Stage 1 初始化CPU/内存/串口 Stage 2 加载完整Bootloader 显示启动菜单 等待用户选择/超时 加载内核 跳转到内核入口 从ROM/Flash执行 从Flash/磁盘加载到RAM 从磁盘/Flash加载到RAM

4.2 GRUB:PC世界的启动管家

GRUB是Linux世界里用得最多的Bootloader。我最早接触GRUB是在2005年配双系统的时候,那时候还是GRUB Legacy。现在主流是GRUB 2,配置方式变化很大。

4.2.1 GRUB的工作流程

GRUB 2的启动过程分三步:

  1. boot.img:放在MBR(主引导记录)里,只有446字节。它的任务就是找到core.img的位置。
  2. core.img:包含文件系统驱动,能识别/boot分区,然后加载完整的GRUB模块。
  3. 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统治嵌入式。理解它们的分阶段设计思想,掌握环境变量和配置文件的用法,就能应对绝大多数启动问题。

可可精品资料库