3. 内核解压与重定位:vmlinuz与zImage的区别、自解压代码(head.S)的执行流程
好,咱们接着往下走。上一章我们把BootLoader的工作聊完了,现在内核镜像已经被加载到了内存里。但这时候你看到的那个文件——不管是叫vmlinuz还是zImage——其实都还不是可以直接运行的内核。说白了,它是一个压缩包,外面裹了一层自解压代码。
我记得刚入行那会儿,有同事直接拿vmlinuz去反汇编,结果发现入口点根本不是start_kernel,而是一堆看不懂的汇编。后来才明白,那其实是解压程序的入口。嗯,今天我们就来扒开这层外衣。
3.1 vmlinuz vs zImage:到底差在哪?
这两个名字,你肯定都见过。很多初学者容易搞混,我简单梳理一下。
| 特性 | vmlinuz | zImage |
|---|---|---|
| 全称 | Virtual Memory LINUX (压缩) | zImage (压缩内核镜像) |
| 压缩方式 | 通常为gzip,也可能是bz2、lzma等 | gzip |
| 加载地址 | 通常加载到高端内存(>1MB) | 加载到低端内存(<1MB) |
| 解压后大小 | 可大于1MB,适合大内核 | 解压后必须小于1MB |
| 适用场景 | 现代x86、ARM等主流架构 | 早期x86、小内存系统 |
| 文件头 | 包含完整的ELF头或自解压头 | 简单的自解压头 |
说白了,zImage是vmlinuz的前身。早期内核小,压缩后能塞进640KB以下的常规内存,所以叫“z”Image(z表示压缩)。后来内核越来越大,1MB都不够用了,就搞出了vmlinuz,可以加载到任意位置。
核心区别一句话:zImage是“小内核专用”,vmlinuz是“大内核通用”。现在你看到的几乎所有发行版内核,都是vmlinuz。
我个人习惯在调试嵌入式系统时,如果内存紧张(比如只有4MB),会特意编一个zImage。但说实话,现在这种场景越来越少了。
3.2 自解压代码的执行流程:head.S 干了什么?
不管是vmlinuz还是zImage,它们的开头都有一段自解压代码。对于x86架构,这段代码在 arch/x86/boot/compressed/head.S 里。ARM架构也有类似的,叫 arch/arm/boot/compressed/head.S。我们今天以x86为例,因为它的逻辑最清晰。
你想想看,CPU拿到vmlinuz后,第一条指令执行的是哪?就是head.S的入口。它要完成的任务其实就三件:
- 设置基本运行环境(栈、段寄存器等)
- 解压内核(调用decompress_kernel)
- 重定位并跳转(把解压后的内核挪到正确位置,然后跳过去)
我画了一张流程图,帮你理清head.S的执行脉络:
3.3 关键代码段解析
我们来看一段head.S的核心片段(简化版,x86 32位):
# arch/x86/boot/compressed/head.S
.code32
.text
startup_32:
# 1. 设置栈
leal (ebx - startup_32)(%ebp), %esp
# 2. 检查是否支持长模式(64位)
movl $0x80000001, %eax
cpuid
testl $0x20000000, %edx
jnz .Llong_mode_supported
# 3. 解压内核
# 参数:输入地址、输出地址、长度等
pushl %ebp # 保存ebp
call decompress_kernel
popl %ebp
# 4. 重定位
# 计算解压后的内核应该放在哪
movl %ebp, %ebx
addl $(z_output_len - _end), %ebx
# 5. 跳转到解压后的内核
leal relocated(%ebx), %eax
jmp *%eax
这段代码看起来不长,但每一步都有讲究。我挑几个重点说说:
3.4 避坑指南:我踩过的几个坑
坑一:栈设置不对,解压直接崩
我曾经在移植内核到一个新的x86板子时,发现解压到一半就挂了。查了两天才发现,是head.S里栈指针没对准。因为自解压代码运行的时候,原来的栈可能已经被覆盖了。所以leal (ebx - startup_32)(%ebp), %esp这行,必须确保ebx指向正确的基址。
坑二:重定位时地址重叠
解压后的内核如果和压缩镜像有重叠,那解压过程中就会把自己覆盖掉。嗯,这听起来像是个死循环。内核的做法是:如果检测到重叠,就先把压缩镜像挪到安全位置,再解压。这个逻辑在decompress_kernel里处理,但如果你自己写BootLoader,一定要留够空间。
警告:vmlinuz的加载地址和解压后的内核地址不能重叠!通常建议解压后的内核地址比压缩镜像高至少2MB。否则你会看到“Data abort”或“General protection fault”。
3.5 解压算法的选择
内核支持多种压缩算法:gzip、bzip2、lzma、xz、lzo、lz4。你可能会问,哪个最好?
我个人的经验是:
- gzip:兼容性最好,几乎所有架构都支持。但压缩率一般。
- lzma:压缩率最高,但解压慢。适合存储空间紧张的系统。
- lz4:解压极快,但压缩率低。适合启动速度要求高的场景。
我记得有一次做车载系统,要求冷启动必须在2秒内进入用户态。我们把压缩算法从gzip换成lz4,解压时间从800ms降到了200ms。虽然镜像大了30%,但启动快了,值!
小技巧:在配置内核时,CONFIG_KERNEL_GZIP、CONFIG_KERNEL_LZ4等选项就是用来选压缩算法的。如果你不确定,就选gzip,不会错。
3.6 重定位的细节
解压完成后,内核镜像其实还在一个“临时位置”。head.S的最后一步,就是把它挪到最终运行地址。这个地址通常由BootLoader通过内核启动参数告诉内核,或者由内核自己计算。
对于x86,最终地址通常是物理地址0x100000(1MB)或更高。对于ARM,则取决于平台的内存映射。
重定位的代码其实就是一个memcpy,但要注意:如果源和目标有重叠,必须用memmove。head.S里用的是rep movsb指令,配合方向标志位,可以正确处理重叠情况。
嗯,这里有个细节:重定位之后,原来的压缩镜像就可以被覆盖了。所以解压后的内核会占据最终位置,而压缩镜像所在的内存会被释放,留给后续的内核使用。
3.7 总结一下
vmlinuz和zImage的区别,说白了就是“能不能装下大内核”。head.S的自解压流程,就是“设置环境→解压→重定位→跳转”四步走。每一步都有坑,但只要你理解了内存布局和地址关系,就不容易出问题。
我个人建议,如果你在做内核移植,第一步就是先确认压缩镜像的加载地址和解压后的目标地址。把这两个地址搞清楚,head.S的执行流程就成功了一半。
公众号:蓝海资料掘金营,微信deep3321