3、内核解压机制:vmlinuz与zImage的区别,解析自解压代码如何将内核镜像放到正确位置

说到内核启动,有个环节特别有意思——解压。你想想看,我们平时编译出来的内核镜像,动不动就几兆甚至十几兆。但嵌入式设备的存储空间和内存都有限,总不能直接塞个大家伙进去吧?所以就有了压缩内核这回事。

我刚开始接触内核时,就被 vmlinuzzImage 这两个名字搞晕过。它们到底啥关系?自解压又是怎么玩的?今天咱们就把它彻底讲清楚。

3.1 vmlinuz 与 zImage:名字背后的故事

先说说这两个东西的本质区别。

vmlinuz 是压缩后的内核镜像。它前面有一段自解压代码,后面跟着压缩过的内核主体。说白了,它就是一个能自己把自己解开的压缩包。

zImage 是 vmlinuz 的一种特殊形式。它被限制在 640KB 以内,专门给那些内存小的老式 x86 系统用。我当年在 486 机器上折腾 Linux 时,就经常跟 zImage 打交道。

现在的主流架构,比如 ARM、ARM64、x86_64,基本都用 vmlinuz 了。zImage 已经很少见了。但它们的解压原理是一样的。

核心区别一句话总结:

  • vmlinuz:通用压缩内核,包含自解压代码
  • zImage:vmlinuz 的小内存版本,限制 640KB
  • bzImage:大内核版本,突破了 640KB 限制,现在 x86 上用的就是它

3.2 自解压代码:内核的"自我救赎"

自解压代码,说白了就是一段小程序。它被链接在压缩内核的前面。系统启动时,这段代码先跑起来,负责把后面的压缩数据解压到正确位置。

我画了一张流程图,帮你理清这个过程:

内核自解压流程 Bootloader 加载 vmlinuz 到内存中的临时位置 自解压代码(head.o)开始执行 确定解压目标地址(通常为 CONFIG_PHYSICAL_START) 检查是否有地址重叠,必要时进行重定位 调用 decompress 函数解压 gzip / lzma / lzo / xz 等算法 将解压后的内核放到目标地址 跳转到解压后的内核入口 start_kernel() 开始执行 关键步骤:地址计算 核心动作:解压

3.3 自解压代码的核心逻辑

自解压代码主要在 arch/x86/boot/compressed/head_64.S(x86_64)或 arch/arm/boot/compressed/head.S(ARM)里。我挑几个关键点说说。

3.3.1 地址计算:别把自己覆盖了

自解压代码要做的第一件事,就是算清楚自己现在在哪,目标地址在哪。如果目标地址和当前地址重叠了,那就麻烦了——解压到一半把自己给覆盖了。

我记得在调试一个 ARM 板子时,就遇到过这个问题。内核镜像加载到了 0x20000000,但解压目标地址也是 0x20000000。结果解压到一半,自解压代码自己先被覆盖了,系统直接崩溃。后来加了重定位逻辑才搞定。

代码里是这样处理的:

// 伪代码示意
if (target_addr == current_addr) {
    // 地址重叠,需要先把自己挪走
    memmove(temp_addr, current_addr, size);
    // 然后从 temp_addr 解压到 target_addr
    decompress(temp_addr, target_addr);
} else {
    // 直接解压
    decompress(current_addr, target_addr);
}

3.3.2 解压算法选择

内核支持多种压缩算法。你可以在配置时选:

算法 压缩比 解压速度 典型场景
gzip 中等 通用,兼容性最好
lzma 存储空间紧张时
lzo 极快 启动速度要求高时
xz 最高 最慢 极致压缩需求

我个人习惯用 lzo。虽然压缩比不高,但解压速度快。嵌入式设备启动时间本来就紧张,能省一点是一点。

3.3.3 解压后的清理工作

解压完成后,自解压代码还要做几件事:

  • 清 BSS 段——把未初始化的全局变量清零
  • 设置页表——如果内核需要虚拟地址映射
  • 跳转到解压后的内核入口——通常是 startup_64start_kernel

调试小技巧:

如果你想看自解压过程到底干了什么,可以在内核启动参数里加 earlyprintk。这样在解压阶段就能看到打印信息。我曾经靠这个定位过一个解压地址算错的 bug。

3.4 实际项目中的避坑指南

做嵌入式开发这么多年,我在内核解压这块踩过不少坑。挑几个典型的说说。

坑一:解压地址与内存布局冲突

我曾经在一个项目里,把内核解压地址设在了 0x80000000。结果那个地址正好是外设寄存器的映射区。解压完一运行,外设全乱套了。后来查了芯片手册,才发现内存区域是 0x80000000 到 0x90000000,但前 16MB 被外设占了。最后把解压地址改到 0x81000000 才搞定。

坑二:压缩算法不匹配

有次我换了内核版本,忘了改压缩算法配置。编译出来的内核用的是 xz 压缩,但 bootloader 里自解压代码只支持 gzip。结果启动时解压失败,卡在 "Decompressing Linux..." 就不动了。嗯,这种问题查起来特别费时间。

3.5 总结一下

内核解压机制,说白了就是一段小程序把自己后面的压缩数据解开,放到正确位置。vmlinuz 和 zImage 的区别主要在于大小限制,但原理一样。

理解了这个过程,你就能明白:

  • 为什么内核镜像要压缩——省存储、省传输时间
  • 为什么自解压代码要处理地址重叠——防止把自己覆盖
  • 为什么不同压缩算法会影响启动速度——解压时间不一样

下次你看到内核启动时打印 "Decompressing Linux...",就知道背后发生了什么事。这感觉,挺踏实的。