4. 内核解压与重定位
当引导加载程序(如GRUB)将内核映像加载到内存后,CPU 首先执行的是位于内核映像前端的解压桩(Decompression Stub)代码。这一阶段的核心任务是将压缩存储的内核主体(vmlinux)解压到正确的位置,并完成地址重定位,最终跳转到内核的真正入口点。理解 vmlinuz 与 initramfs 的布局,以及 decompress_kernel 和 startup_32/64 的执行流,是排查早期启动崩溃的关键。
4.1 vmlinuz 与 initramfs 的复合结构
通常位于 /boot 目录下的 vmlinuz-* 文件并非一个单一的内核映像,而是一个自解压的复合体。它由以下三部分顺序拼接而成:
| 组成部分 | 说明 | 典型大小 |
|---|---|---|
| 1. 解压桩头部 | 包含 startup_32(或 startup_64)入口、引导协议版本、扇区数等元数据。这是 CPU 最先执行的代码。 |
约 1~4 个扇区 |
| 2. 压缩的内核主体 | 经过 gzip / xz / lz4 等算法压缩的 vmlinux 二进制数据。这是真正的内核代码和数据。 |
数 MB 到十几 MB |
| 3. 嵌入式 initramfs | 可选的,在构建内核时通过 CONFIG_INITRAMFS_SOURCE 嵌入的 cpio 归档。如果存在,它紧跟在压缩内核之后。 |
取决于配置 |
调试提示: 使用
binwalk vmlinuz-$(uname -r) 可以直观地看到这三个部分的偏移和大小。如果引导加载程序报告“Invalid magic number”,通常意味着解压桩头部被破坏或加载地址错误。
下图展示了 vmlinuz 在磁盘上的静态布局与加载到内存后的动态布局关系:
4.2 解压过程:decompress_kernel
解压桩的核心逻辑位于 arch/x86/boot/compressed/misc.c 中的 decompress_kernel() 函数。该函数执行以下关键步骤:
- 初始化控制台与视频模式:如果启用了早期串口或 VGA 输出,会在此处初始化,以便输出解压进度信息(例如“Decompressing Linux...”)。
- 选择解压算法:根据内核编译时选择的算法(gzip/xz/lz4/lzo),调用对应的解压函数。现代内核通常使用
__decompress()统一接口。 - 执行解压:将压缩数据从源地址(
input_data)解压到目标地址(output)。目标地址通常是LOAD_PHYSICAL_ADDR(对于 x86_64 通常是0x1000000,即 16MB)。 - 返回解压后大小:函数返回解压后的内核大小,供后续重定位使用。
关键代码路径(x86_64):
// arch/x86/boot/compressed/misc.c
asmlinkage __visible void *decompress_kernel(void *rmode, memptr heap,
unsigned char *input_data,
unsigned long input_len,
unsigned char *output,
unsigned long *output_len)
{
// 1. 初始化早期串口/视频输出
console_init();
// 2. 显示解压进度
debug_putstr("Decompressing Linux... \n");
// 3. 执行解压
*output_len = __decompress(input_data, input_len, NULL, NULL,
output, 0, NULL, error_putstr);
// 4. 返回解压后的内核地址
return output;
}
常见崩溃点: 如果
output 地址与加载地址重叠,或者 input_data 指针指向了无效内存(例如被 BIOS 或引导加载程序覆盖),解压过程会触发 #GP(一般保护故障) 或 #PF(页故障)。此时屏幕上通常只看到“Decompressing Linux...”后立即死机。
4.3 重定位与入口点:startup_32 / startup_64
解压完成后,内核需要将自己“搬运”到最终运行的物理地址,并设置好页表(如果启用了分页)。这一过程由汇编入口点 startup_32(32 位)或 startup_64(64 位)完成。
4.3.1 startup_32(32 位内核)
- 执行环境:CPU 处于 32 位保护模式,分页可能未开启。
- 主要任务:
- 设置段寄存器(DS/ES/FS/GS/SS)为已知状态。
- 建立临时页表(如果内核配置了
CONFIG_X86_PAE或CONFIG_X86_64)。 - 调用
decompress_kernel()。 - 如果解压后的目标地址与当前运行地址不同,执行重定位:使用
memcpy或rep movsb将解压后的内核复制到LOAD_PHYSICAL_ADDR。 - 跳转到解压后内核的入口点(
startup_32的第二个副本,位于解压后的 vmlinux 开头)。
4.3.2 startup_64(64 位内核)
- 执行环境:CPU 处于 64 位长模式,但页表可能只覆盖了低 1MB 或 4MB 区域。
- 主要任务:
- 建立覆盖整个物理内存的恒等映射页表(Identity Map),确保解压后的内核在任意物理地址都能被寻址。
- 调用
decompress_kernel()。 - 执行内核重定位:如果内核被配置为可重定位(
CONFIG_RELOCATABLE),解压桩会根据引导加载程序传递的load_addr计算新的运行地址,并调整内核中的符号表。 - 跳转到
startup_64的最终入口(位于解压后的 vmlinux 中)。
下图展示了从引导加载程序跳转到 startup_32/64 开始,到最终进入解压后内核的完整流程:
4.4 重定位细节:CONFIG_RELOCATABLE 与 KASLR
现代内核默认启用 CONFIG_RELOCATABLE,这意味着内核可以在任何物理地址运行,而不必固定在 0x1000000。这为 KASLR(内核地址空间布局随机化) 提供了基础。
| 特性 | 作用 | 调试影响 |
|---|---|---|
| CONFIG_RELOCATABLE | 内核在解压后根据加载地址调整所有符号引用,使得内核可以在任意物理地址运行。 | 如果引导加载程序传递了错误的加载地址,内核会在重定位时崩溃。检查 boot_params.hdr.code32_start 的值。 |
| KASLR | 在重定位的基础上,随机选择一个偏移量,将内核放置在随机地址,增加安全性。 | 调试时建议在 GRUB 命令行添加 nokaslr 禁用随机化,以获得可预测的地址。 |
重定位的核心代码(arch/x86/boot/compressed/head_64.S):
/* 计算重定位偏移量 */
leaq rva(_bss)(%rbp), %rbx /* 解压后内核的 _bss 地址 */
subq $LOAD_PHYSICAL_ADDR, %rbx /* 计算偏移量 */
/* 如果偏移量为 0,则跳过重定位 */
testq %rbx, %rbx
jz relocated
/* 否则,遍历重定位表,调整每个条目 */
leaq rva(rela_start)(%rbp), %rsi
leaq rva(rela_end)(%rbp), %rdi
/* ... 循环处理每个 Elf64_Rela 条目 ... */
4.5 常见问题排查清单
- 问题: 引导加载程序加载后,屏幕无任何输出,直接重启或死机。
- 排查: 检查
vmlinuz文件是否完整(sha256sum对比)。检查 GRUB 配置中的linux命令指定的文件路径和分区是否正确。
- 排查: 检查
- 问题: 输出“Decompressing Linux... done”后立即崩溃。
- 排查: 解压后的内核大小超过了可用内存?尝试添加
mem=参数限制内存范围。检查LOAD_PHYSICAL_ADDR是否与硬件保留区域(如 IOMMU、ACPI NVS)冲突。
- 排查: 解压后的内核大小超过了可用内存?尝试添加
- 问题: 输出“Booting the kernel.”后无响应。
- 排查: 重定位后的入口点地址错误。使用
gdb连接 QEMU 的-s端口,在startup_64处设置断点,检查%rip是否指向解压后的内核头部。
- 排查: 重定位后的入口点地址错误。使用
- 问题: 启用了 KASLR 的系统在早期启动时随机崩溃。
- 排查: 在 GRUB 中添加
nokaslr排除随机化因素。如果问题消失,可能是 KASLR 选择的地址与硬件资源冲突,需要检查e820内存映射。
- 排查: 在 GRUB 中添加
实战技巧: 使用 QEMU 进行调试时,可以结合
-kernel vmlinuz -initrd initrd.img -append "earlyprintk=serial,ttyS0,115200 console=ttyS0" -serial stdio 来完整捕获从解压桩到内核初始化的所有输出。如果怀疑是重定位问题,添加 nokaslr 并指定 mem=512M 来缩小排查范围。