4. 内核解压与重定位:vmlinuz与initramfs结构,解压过程(decompress_kernel),重定位与入口点(startup_32/64)

当引导加载程序(如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 在磁盘上的静态布局与加载到内存后的动态布局关系:

vmlinuz 与 initramfs 内存布局 磁盘 /boot/vmlinuz 布局 解压桩头部 压缩的 vmlinux 主体 嵌入式 initramfs 加载到内存后的布局(解压前) 解压桩代码 压缩的 vmlinux 主体 嵌入式 initramfs 解压后(重定位到 LOAD_PHYSICAL_ADDR) 解压后的 vmlinux(.text, .data, .bss) initramfs 说明:解压桩代码在解压完成后通常会被覆盖,initramfs 在解压后会被移动到内核末尾的保护区。

4.2 解压过程:decompress_kernel

解压桩的核心逻辑位于 arch/x86/boot/compressed/misc.c 中的 decompress_kernel() 函数。该函数执行以下关键步骤:

  1. 初始化控制台与视频模式:如果启用了早期串口或 VGA 输出,会在此处初始化,以便输出解压进度信息(例如“Decompressing Linux...”)。
  2. 选择解压算法:根据内核编译时选择的算法(gzip/xz/lz4/lzo),调用对应的解压函数。现代内核通常使用 __decompress() 统一接口。
  3. 执行解压:将压缩数据从源地址(input_data)解压到目标地址(output)。目标地址通常是 LOAD_PHYSICAL_ADDR(对于 x86_64 通常是 0x1000000,即 16MB)。
  4. 返回解压后大小:函数返回解压后的内核大小,供后续重定位使用。
关键代码路径(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 位保护模式,分页可能未开启。
  • 主要任务:
    1. 设置段寄存器(DS/ES/FS/GS/SS)为已知状态。
    2. 建立临时页表(如果内核配置了 CONFIG_X86_PAE 或 CONFIG_X86_64)。
    3. 调用 decompress_kernel()。
    4. 如果解压后的目标地址与当前运行地址不同,执行重定位:使用 memcpy 或 rep movsb 将解压后的内核复制到 LOAD_PHYSICAL_ADDR。
    5. 跳转到解压后内核的入口点(startup_32 的第二个副本,位于解压后的 vmlinux 开头)。

4.3.2 startup_64(64 位内核)

  • 执行环境:CPU 处于 64 位长模式,但页表可能只覆盖了低 1MB 或 4MB 区域。
  • 主要任务:
    1. 建立覆盖整个物理内存的恒等映射页表(Identity Map),确保解压后的内核在任意物理地址都能被寻址。
    2. 调用 decompress_kernel()。
    3. 执行内核重定位:如果内核被配置为可重定位(CONFIG_RELOCATABLE),解压桩会根据引导加载程序传递的 load_addr 计算新的运行地址,并调整内核中的符号表。
    4. 跳转到 startup_64 的最终入口(位于解压后的 vmlinux 中)。

下图展示了从引导加载程序跳转到 startup_32/64 开始,到最终进入解压后内核的完整流程:

内核解压与重定位流程图 引导加载程序跳转 startup_32 / startup_64 建立临时页表 / 恒等映射 decompress_kernel() 重定位(复制 + 符号调整) 跳转到解压后内核入口 调试要点: • 如果死机在“Decompressing Linux...”之前:检查 startup_32/64 的页表设置或段寄存器。 • 如果死机在“Decompressing Linux...”之后:检查解压目标地址是否与硬件保留区(如 ACPI 表)冲突。 • 使用 earlyprintk=serial,ttyS0,115200 可以捕获此阶段的输出。

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 内存映射。
实战技巧: 使用 QEMU 进行调试时,可以结合 -kernel vmlinuz -initrd initrd.img -append "earlyprintk=serial,ttyS0,115200 console=ttyS0" -serial stdio 来完整捕获从解压桩到内核初始化的所有输出。如果怀疑是重定位问题,添加 nokaslr 并指定 mem=512M 来缩小排查范围。