1. 内核启动入口分析:从BIOS/UEFI到GRUB
说实话,很多做嵌入式Linux开发的朋友,一上来就盯着start_kernel()看。但我要说,内核的第一行代码执行前,已经发生了太多关键的事情。不理解这些,你调试启动问题时会非常痛苦。
我个人习惯把x86架构的启动过程分成三个阶段:固件阶段(BIOS/UEFI)、引导阶段(GRUB)、内核入口阶段。今天我们先聊前两个阶段,看看内核的第一行代码执行前,CPU到底在忙什么。
1.1 按下电源键的那一刻
你按下电源键,CPU复位,所有寄存器回到默认值。这时候CPU处于实模式(Real Mode),地址线只有20位有效,最大寻址1MB。嗯,这里要注意:CS:IP = 0xF000:0xFFF0,这个地址映射到BIOS ROM的入口。
关键事实:CPU复位后第一条指令地址是0xFFFFFFF0(对于现代CPU),但为了兼容性,这个地址被映射到BIOS ROM区域。BIOS/UEFI从这里开始执行。
我在项目中遇到过一个问题:某款工控主板在低温环境下启动失败,抓了半天发现是BIOS自检阶段内存初始化时序不对。你看,连BIOS阶段都可能出问题,更别说后面的内核了。
1.2 BIOS vs UEFI:到底有什么区别?
说白了,BIOS是爷爷辈的技术,UEFI是后来居上的方案。我建议你至少了解以下几点区别:
| 特性 | BIOS (Legacy) | UEFI |
|---|---|---|
| 启动模式 | 实模式,16位 | 保护模式/64位 |
| 分区表 | MBR(主引导记录) | GPT(GUID分区表) |
| 引导加载器位置 | MBR(512字节) | EFI系统分区(ESP) |
| 可扩展性 | 差,只能从磁盘启动 | 好,支持网络启动、驱动加载 |
| 安全启动 | 不支持 | 支持(Secure Boot) |
你想想看,BIOS那512字节的MBR能干什么?只能放一段很小的引导代码,然后跳转到下一个阶段。而UEFI可以直接加载完整的引导管理器,甚至能在启动过程中加载驱动——这就是为什么现代系统启动时能看到图形界面。
1.3 GRUB到底做了什么?
GRUB(GRand Unified Bootloader)是目前Linux最常用的引导加载器。它的核心任务就三个:
- 加载内核镜像到内存
- 设置启动参数(如root分区、initrd路径)
- 切换到保护模式(或长模式),然后跳转到内核入口
我曾经调试过一个启动问题:GRUB加载内核后直接重启,没有任何错误信息。后来发现是initrd镜像太大,超出了GRUB能加载的内存范围。嗯,这种问题在嵌入式设备上特别常见。
调试技巧:在GRUB菜单按'e'编辑启动项,可以手动修改内核参数。按'c'进入GRUB命令行,用ls、cat等命令检查文件系统。这些在排查启动问题时非常有用。
1.4 内核入口前的内存布局
当GRUB把控制权交给内核时,内存已经不再是刚开机时的样子了。我习惯用下面这张图来理解:
这张图是我自己总结的,每次调试启动问题我都会先确认内核被加载到了哪个地址。你想想看,如果GRUB把内核加载到了错误的位置,那后面的解压和跳转全都会乱套。
1.5 从实模式到保护模式的切换
这是x86启动过程中最微妙的一步。GRUB在跳转到内核之前,必须完成:
- 关闭中断(CLI指令)
- 加载GDT(全局描述符表)
- 设置CR0寄存器的保护模式位(PE位)
- 远跳转(far jump)刷新指令流水线
注意:如果GDT设置错误,或者段描述符的权限位不对,CPU会立即触发异常。我曾经见过一个案例:某定制内核在切换模式时崩溃,最后发现是GRUB版本太旧,生成的GDT不兼容新CPU。
内核的setup代码(arch/x86/boot/header.S)会检查当前CPU模式。如果GRUB已经切到了保护模式,内核就直接接管;如果还在实模式,内核自己再切一次。嗯,这里有个细节:内核的setup代码本身是16位实模式代码,它运行在0x7C00附近,然后才跳转到32位入口。
1.6 内核的第一行代码到底在哪?
很多人以为start_kernel()是第一行。其实不是。真正的第一行代码在arch/x86/boot/header.S中,具体是:
// arch/x86/boot/header.S (简化)
_start:
.byte 0xeb // 短跳转指令
.byte start_of_setup-1f // 跳转到setup代码
// ... 后面是内核头部信息(boot_params结构)
start_of_setup:
// 这里才是真正的入口
movw %ds, %ax
movw %ax, %ss
movw $__BOOT_STACK, %sp // 设置栈指针
// ... 初始化段寄存器、检查CPU特性等
这段代码运行在实模式下,主要做几件事:
- 设置栈指针(很重要,否则call指令会乱跳)
- 检查CPU是否支持保护模式(CPUID指令)
- 加载GDT并切换到保护模式
- 跳转到32位入口(startup_32)
我个人习惯在调试时,用QEMU配合GDB在0x7C00处设断点,然后单步跟踪这段setup代码。你会发现很多有趣的事情——比如BIOS可能会修改某些内存区域,或者GRUB传递的参数格式不对。
调试命令参考:
# 使用QEMU + GDB调试内核启动
qemu-system-x86_64 -kernel bzImage -s -S
# 在另一个终端
gdb vmlinux
(gdb) target remote :1234
(gdb) hbreak *0x7C00
(gdb) continue
这样你就可以看到内核的第一条指令是如何执行的。
1.7 避坑指南:我踩过的几个坑
做嵌入式Linux这些年,我在启动阶段踩过不少坑。分享几个典型的:
- 坑1:GRUB找不到内核 —— 分区表格式不对。BIOS模式需要MBR,UEFI需要GPT。我曾经把UEFI主板设成了Legacy模式,结果GRUB死活不认GPT分区。
- 坑2:内核解压失败 —— 内存不足。有些嵌入式设备只有128MB内存,如果initrd太大,解压时直接OOM。解决方案是用更小的initramfs,或者用gzip而不是xz压缩。
- 坑3:Secure Boot导致无法启动 —— 内核签名不对。UEFI的Secure Boot会检查引导加载器和内核的签名。我建议在开发阶段先关掉Secure Boot,等稳定了再配置签名。
嗯,这些坑说起来简单,但排查起来真的很费时间。所以我建议你在做新板子的bring-up时,先确保BIOS/UEFI阶段没问题,再往下走。
1.8 小结
从按下电源键到内核的第一行代码,中间经历了:CPU复位 → BIOS/UEFI自检 → 加载GRUB → GRUB加载内核 → 切换到保护模式 → 跳转到setup代码。每一步都可能出问题,每一步都需要你理解底层原理。
我个人觉得,理解启动流程最好的方法就是动手调试。用QEMU模拟一个环境,从BIOS阶段开始单步跟踪,看看每条指令做了什么。你会发现很多书本上没写的东西。
下一章我们会深入内核的setup代码,看看它是如何从实模式切换到保护模式,然后跳转到startup_32的。到时候我会分享一些具体的调试案例。
公众号:蓝海资料掘金营,微信deep3321