4. 内核解压:vmlinuz 与 zImage 的压缩格式、解压入口函数 (decompress_kernel)

当引导加载程序(如 GRUB、U-Boot)将内核映像加载到内存后,CPU 首先执行的并非 C 语言编写的内核初始化代码,而是一段位于内核映像前端的自解压代码(decompressor stub)。这段代码负责将压缩后的内核主体解压到指定的内存地址,然后跳转到解压后的内核入口。理解这一过程,是分析启动日志中“Decompressing Linux... done”背后原理的关键。

4.1 内核映像的两种形态:vmlinux vs. vmlinuz / zImage

在深入解压机制前,必须区分内核的两种基本形态:

形态 文件类型 是否压缩 典型文件名 用途
vmlinux ELF 可执行文件 否(原始未压缩) vmlinux 调试、符号分析、生成压缩映像的输入
vmlinuz 自解压二进制 是(gzip / LZMA / XZ / LZO / Zstd) vmlinuz-5.15.0 实际引导加载程序加载的目标文件(x86)
zImage 自解压二进制(小内核) 是(gzip) zImage ARM 等嵌入式平台,旧式 x86(< 512KB)
bzImage 自解压二进制(大内核) 是(gzip / LZMA / XZ / LZO / Zstd) bzImage 现代 x86 标准格式(big zImage)
关键区分: 在 x86 平台上,vmlinuz 通常就是 bzImage 的符号链接或同名文件。ARM 平台则常使用 zImage 或 uImage(带 U-Boot 头)。无论名称如何,其核心结构都是“解压头 + 压缩内核数据”。

4.2 压缩格式演进与选择

Linux 内核支持多种压缩算法,每种算法在压缩比、解压速度和内存占用上各有取舍。内核构建时通过 CONFIG_KERNEL_* 系列配置项选择。

压缩算法 内核配置项 压缩比(越小越好) 解压速度 典型场景
Gzip CONFIG_KERNEL_GZIP 中等(约 2.5:1) 快 兼容性最好,默认选项
LZMA CONFIG_KERNEL_LZMA 高(约 3.5:1) 慢 嵌入式、存储空间极度受限
XZ CONFIG_KERNEL_XZ 最高(约 4:1) 最慢 追求最小映像体积
LZO CONFIG_KERNEL_LZO 低(约 2:1) 极快 追求快速启动,解压速度优先
Zstd CONFIG_KERNEL_ZSTD 高(约 3:1) 快(接近 LZO) 现代内核推荐,平衡体积与速度
启动日志线索: 在 dmesg 或串口输出中,解压阶段通常会打印类似 Decompressing Linux... done. Booting the kernel. 的信息。如果系统启用了 CONFIG_DEBUG_KERNEL 和 CONFIG_KERNEL_UNCOMPRESS,还可能看到具体的算法标识。

4.3 解压入口函数:decompress_kernel

解压过程的 C 语言入口是 decompress_kernel() 函数,它位于内核源码的 arch/<arch>/boot/compressed/ 目录下。该函数由汇编语言编写的启动头(head.S 或 head_32.S / head_64.S)在设置好基本环境后调用。

4.3.1 函数原型与核心逻辑

// 位于 arch/x86/boot/compressed/misc.c (x86 架构)
// 其他架构类似,但细节不同

asmlinkage __visible void *decompress_kernel(
    void *rmode,        // 指向实模式启动参数的指针 (x86 特有)
    unsigned char *input_data,   // 压缩内核数据的起始地址
    unsigned long input_len,     // 压缩数据的长度
    unsigned char *output_data,  // 解压后内核的目标地址
    unsigned long output_len,    // 解压后内核的预期长度
    unsigned long run_size       // 解压后内核的运行大小 (包含 .bss)
)
{
    // 1. 初始化解压器(根据编译时选择的算法)
    __decompress(input_data, input_len, NULL, NULL,
                 output_data, output_len, NULL, error);
    // 2. 返回解压后内核的入口地址(通常就是 output_data 的起始地址)
    return output_data;
}

执行流程可概括为以下步骤:

  1. 环境准备: 汇编头设置栈指针、清除 BSS 段、建立页表(如果启用了 KASLR)。
  2. 调用 decompress_kernel: 传入压缩数据地址、输出地址等参数。
  3. 算法分发: __decompress 宏根据 CONFIG_KERNEL_* 展开为对应的解压函数(如 gunzip、lzma_decompress、xz_decompress 等)。
  4. 解压完成: 返回解压后内核的入口点,汇编头随后跳转到该地址。
内存覆盖风险: 在解压过程中,输入缓冲区(压缩数据)和输出缓冲区(解压后数据)可能存在重叠。解压代码必须确保输出地址不会覆盖尚未读取的压缩数据。x86 的 bzImage 通过将输出地址设置在输入地址之后来避免此问题;ARM 的 zImage 则通过计算偏移量来保证安全。

4.4 解压流程全景图

下图展示了从引导加载程序跳转到内核映像后,到解压完成并跳转到内核主入口的完整流程:

内核自解压流程架构图 内核自解压流程架构图 引导加载程序 (GRUB / U-Boot) 内核映像头部 (setup.bin / 实模式代码) 解压桩 (head.S) (设置栈 / 页表 / KASLR) decompress_kernel() 选择解压算法 → 调用 __decompress (Gzip / LZMA / XZ / LZO / Zstd) 解压算法执行 压缩数据 → 解压 → 写入 output_data 跳转到解压后内核入口 (start_kernel 的汇编入口) 关键日志输出点 • "Decompressing Linux..." • "done. Booting the kernel." • 若启用 DEBUG:显示算法和进度

4.5 解压过程中的关键日志与调试方法

在启动早期,由于内核的串口或显示驱动尚未初始化,解压阶段的输出通常通过架构特定的早期打印机制实现。以下是常见的调试手段:

  • 早期串口输出(Early Console): 在 x86 上通过 earlyprintk=serial,ttyS0,115200 内核参数启用;在 ARM 上通过 earlycon 或 earlyprintk 启用。这能在解压阶段输出调试信息。
  • 解压进度指示: 内核解压代码会每解压一定量的数据就输出一个 .(点号),最终输出 done。如果看到大量点号后卡住,说明解压算法正在工作但可能陷入死循环或内存错误。
  • KASLR 偏移量: 如果启用了内核地址空间布局随机化(KASLR),解压桩会在解压前计算随机偏移,并在日志中打印 KASLR: Found a good spot at ...。
实战调试技巧: 当内核在解压阶段崩溃(无任何输出或输出 "Decompressing Linux..." 后挂死),可以尝试:
1. 更换压缩算法(如从 XZ 改为 Gzip),排除算法实现问题。
2. 关闭 KASLR(添加 nokaslr 内核参数),排除地址随机化导致的内存冲突。
3. 使用 QEMU + GDB 在 decompress_kernel 入口处设置断点,单步跟踪解压循环。

4.6 不同架构的解压入口差异

架构 解压桩汇编文件 C 入口文件 特殊处理
x86 (32/64) arch/x86/boot/compressed/head_32.S
head_64.S
arch/x86/boot/compressed/misc.c 需要从实模式切换到保护模式/长模式;支持 KASLR
ARM (32-bit) arch/arm/boot/compressed/head.S arch/arm/boot/compressed/misc.c 需要处理自搬移(避免覆盖);支持 ATAG 或 DTB 传递
ARM64 (AArch64) arch/arm64/boot/compressed/head.S arch/arm64/boot/compressed/misc.c 需要处理页表切换;支持 KASLR
RISC-V arch/riscv/boot/compressed/head.S arch/riscv/boot/compressed/misc.c 相对较新,实现借鉴 ARM64

4.7 解压阶段常见问题与排查

问题 1:解压后内核无法启动(无任何输出)
可能原因: 解压后的内核入口地址错误;输出缓冲区与输入缓冲区重叠导致数据损坏;KASLR 随机化后跳转地址计算错误。
排查方法: 在 decompress_kernel 返回后,检查返回地址是否与 output_data 一致;使用 nokaslr 参数排除随机化影响。
问题 2:解压过程中止("Decompressing Linux... " 后无响应)
可能原因: 压缩数据损坏(引导加载程序读取错误);内存不足(输出缓冲区被其他数据占用);解压算法内部错误(罕见)。
排查方法: 验证内核映像的校验和(如 sha256sum);增加 earlyprintk 输出,观察解压循环是否在特定位置卡住。

4.8 小结

内核解压阶段是启动流程中第一个由 C 语言主导的复杂逻辑。理解 decompress_kernel 的调用上下文、压缩算法的选择以及不同架构的实现差异,对于分析启动失败、优化启动时间以及进行底层调试至关重要。下一阶段,解压完成后的内核将进入架构无关的 start_kernel 函数,开启 Linux 内核的全面初始化。