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;
}
执行流程可概括为以下步骤:
- 环境准备: 汇编头设置栈指针、清除 BSS 段、建立页表(如果启用了 KASLR)。
- 调用 decompress_kernel: 传入压缩数据地址、输出地址等参数。
- 算法分发:
__decompress宏根据CONFIG_KERNEL_*展开为对应的解压函数(如gunzip、lzma_decompress、xz_decompress等)。 - 解压完成: 返回解压后内核的入口点,汇编头随后跳转到该地址。
内存覆盖风险: 在解压过程中,输入缓冲区(压缩数据)和输出缓冲区(解压后数据)可能存在重叠。解压代码必须确保输出地址不会覆盖尚未读取的压缩数据。x86 的 bzImage 通过将输出地址设置在输入地址之后来避免此问题;ARM 的 zImage 则通过计算偏移量来保证安全。
4.4 解压流程全景图
下图展示了从引导加载程序跳转到内核映像后,到解压完成并跳转到内核主入口的完整流程:
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(添加
3. 使用 QEMU + GDB 在
1. 更换压缩算法(如从 XZ 改为 Gzip),排除算法实现问题。
2. 关闭 KASLR(添加
nokaslr 内核参数),排除地址随机化导致的内存冲突。3. 使用 QEMU + GDB 在
decompress_kernel 入口处设置断点,单步跟踪解压循环。
4.6 不同架构的解压入口差异
| 架构 | 解压桩汇编文件 | C 入口文件 | 特殊处理 |
|---|---|---|---|
| x86 (32/64) | arch/x86/boot/compressed/head_32.Shead_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 随机化后跳转地址计算错误。
排查方法: 在
可能原因: 解压后的内核入口地址错误;输出缓冲区与输入缓冲区重叠导致数据损坏;KASLR 随机化后跳转地址计算错误。
排查方法: 在
decompress_kernel 返回后,检查返回地址是否与 output_data 一致;使用 nokaslr 参数排除随机化影响。
问题 2:解压过程中止("Decompressing Linux... " 后无响应)
可能原因: 压缩数据损坏(引导加载程序读取错误);内存不足(输出缓冲区被其他数据占用);解压算法内部错误(罕见)。
排查方法: 验证内核映像的校验和(如
可能原因: 压缩数据损坏(引导加载程序读取错误);内存不足(输出缓冲区被其他数据占用);解压算法内部错误(罕见)。
排查方法: 验证内核映像的校验和(如
sha256sum);增加 earlyprintk 输出,观察解压循环是否在特定位置卡住。
4.8 小结
内核解压阶段是启动流程中第一个由 C 语言主导的复杂逻辑。理解 decompress_kernel 的调用上下文、压缩算法的选择以及不同架构的实现差异,对于分析启动失败、优化启动时间以及进行底层调试至关重要。下一阶段,解压完成后的内核将进入架构无关的 start_kernel 函数,开启 Linux 内核的全面初始化。