3、内核镜像的构成:vmlinux vs zImage vs uImage,压缩与解压缩的奥秘,DTB设备树的位置

好,咱们今天聊聊内核镜像本身。你编译完内核,会生成一堆文件,到底哪个才是我们要烧进去的?vmlinux、zImage、uImage,还有那个神秘的DTB,它们之间到底是什么关系?

我刚开始接触嵌入式Linux时,也被这些名字搞晕过。有一次项目赶进度,我直接把vmlinux扔给uboot去启动,结果uboot告诉我“不认识这玩意儿”。嗯,从那以后,我才老老实实去研究这些镜像格式的区别。

3.1 从vmlinux说起

vmlinux,这是最原始的内核镜像。它就是一个ELF格式的可执行文件,包含了完整的符号表、调试信息,说白了就是编译出来的“原汁原味”的内核。

它的特点很明显:

  • 体积巨大——一个典型的内核,vmlinux可能有几十MB甚至上百MB
  • 包含调试信息——方便你gdb调试,但生产环境用不上
  • 不能直接启动——大多数bootloader不认识ELF格式

我个人习惯把vmlinux当作“调试用”的镜像。平时做内核开发、调试驱动时,我会用vmlinux配合gdb来单步跟踪。但真正要部署到板子上,得用它的“压缩版”。

核心区别:vmlinux是ELF格式,而zImage/uImage是经过处理后的二进制镜像,去掉了符号表,可能还做了压缩。

3.2 zImage:自解压的智慧

zImage,全称是“zapped image”或者“compressed image”。它是在vmlinux的基础上,经过objcopy去掉ELF头,再用gzip压缩,最后加上一个自解压头形成的。

为什么要有自解压头?你想想看,bootloader把内核加载到内存后,内核是一坨压缩数据,CPU没法直接执行。自解压头就是一段小程序,它先运行起来,把后面的压缩数据解压到内存的另一个位置,然后跳转过去执行。

zImage的启动流程大致是这样的:

  1. bootloader把zImage加载到内存某个地址(比如0x30008000)
  2. CPU从zImage的开头执行,那里是自解压代码
  3. 自解压代码把后面的gzip数据解压到另一个地址(通常是高地址)
  4. 解压完成后,跳转到解压后的内核入口

我在项目中遇到过一个问题:zImage解压时,目标地址和源地址有重叠,导致解压失败。后来查资料才知道,自解压代码会检测这种情况,如果重叠,它会先把自己挪到安全位置再解压。嗯,这个细节很多新手会忽略。

小技巧:你可以用file命令查看一个镜像到底是什么格式。zImage通常会被识别为“Linux kernel ARM boot executable zImage”。

3.3 uImage:uboot的专属格式

uImage,其实就是zImage加上一个64字节的uboot头。这个头里包含了:

  • 镜像类型(内核、ramdisk、DTB等)
  • 加载地址
  • 入口地址
  • 镜像大小
  • CRC校验值

uboot通过这个头来判断“我该把这个镜像加载到哪个地址?入口在哪里?数据有没有损坏?”

生成uImage的命令很简单:

mkimage -A arm -O linux -T kernel -C gzip -a 0x30008000 -e 0x30008040 -n "Linux kernel" -d zImage uImage

注意这里的-a和-e参数。-a是加载地址,-e是入口地址。为什么入口地址比加载地址多了0x40?因为前面64字节是uboot头,真正的zImage从0x40偏移开始。

我曾经踩过的坑:有一次我忘了改-e参数,直接用了默认值,结果uboot跳转到了uboot头的位置,CPU执行了一堆乱码,板子直接死机。所以,入口地址一定要指向zImage的开头,而不是uImage的开头。

3.4 压缩与解压缩的奥秘

内核支持多种压缩算法:gzip、bzip2、lzma、lzo、lz4等。不同的算法在压缩比和解压速度上各有千秋。

算法 压缩比 解压速度 适用场景
gzip 中等 中等 通用,兼容性最好
bzip2 高 慢 追求最小体积
lzma 很高 较慢 存储空间极度受限
lzo 低 很快 追求快速启动
lz4 低 极快 启动时间敏感

我个人习惯在开发阶段用gzip,兼容性好,出问题容易排查。量产阶段如果对启动时间有要求,我会换成lz4。有一次做车载项目,要求冷启动必须在3秒内看到界面,我把内核从gzip换成lz4,解压时间从800ms降到了200ms,效果很明显。

解压缩的代码在内核源码的arch/arm/boot/compressed目录下。你可以看到head.S、misc.c这些文件,它们就是自解压的核心。如果你对解压过程感兴趣,可以读读head.S,里面全是汇编,但逻辑很清晰。

3.5 DTB设备树的位置

DTB(Device Tree Blob)是设备树的二进制形式。它描述了硬件信息:CPU、内存、外设、中断、GPIO等等。内核启动时需要DTB来知道“我跑在什么硬件上”。

DTB可以放在三个地方:

  1. 附加在内核镜像后面——最常见的方式,uboot把内核和DTB一起加载
  2. 单独存放在存储分区——比如放在FAT分区里,uboot先加载DTB到内存
  3. 编译进内核——在配置内核时指定DTB,它会和内核打包在一起

对于第一种方式,uboot的命令通常是这样的:

tftp 0x30008000 zImage
tftp 0x31000000 myboard.dtb
bootz 0x30008000 - 0x31000000

这里bootz命令的第三个参数就是DTB的地址。内核启动时,会从这个地址解析DTB,获取硬件信息。

我曾经犯过一个低级错误:DTB和内核的加载地址太近,导致解压时内核覆盖了DTB。结果内核启动到一半,访问DTB时发现数据全是乱的,直接panic。后来我养成了习惯,DTB至少放在内核地址后面2MB的位置,留足安全空间。

重要提醒:DTB的版本必须和内核匹配。如果你用5.10的内核,却加载了4.19的DTB,很可能某些外设无法正常工作。因为设备树绑定(binding)在不同版本间可能有变化。

3.6 一张图看懂内核镜像的构成

下面我用一张SVG图来总结vmlinux、zImage、uImage和DTB的关系:

vmlinux ELF格式,含符号表、调试信息 体积大,不能直接启动 objcopy + gzip zImage 自解压头 + gzip压缩数据 bootloader可直接加载执行 mkimage uImage uboot头(64B) + zImage 包含加载地址、入口、CRC DTB (设备树) 描述硬件信息的二进制文件 可附加在镜像后或单独加载 启动流程 ① bootloader 加载zImage/uImage 到内存 ② 自解压头 解压gzip数据 到高地址 ③ 内核启动 解析DTB 初始化硬件 ④ 挂载 根文件系统 启动init

这张图把整个流程串起来了。从vmlinux开始,经过objcopy和压缩变成zImage,再通过mkimage加上uboot头变成uImage。DTB可以独立存在,也可以和内核一起加载。启动时,bootloader把镜像加载到内存,自解压头先运行,解压出真正的内核,然后内核解析DTB,初始化硬件,最后挂载根文件系统。

嗯,到这里,内核镜像的构成就讲完了。你可能会问:“那我到底该用zImage还是uImage?”我的建议是:如果你的bootloader是uboot,用uImage;如果是其他bootloader(比如barebox、grub),用zImage。至于vmlinux,留着调试用就好。

一句话总结:vmlinux是原材料,zImage是半成品,uImage是成品,DTB是说明书。没有说明书,成品也跑不起来。