4. Kernel 分区打包:zImage/Image.gz、DTB 打包、内核启动参数与 cmdline

Kernel 分区,是整个镜像打包里最需要小心伺候的部分。我见过太多人把精力全放在 rootfs 上,结果 Kernel 分区出问题,板子直接变砖。说白了,Kernel 分区就是系统的发动机,发动机装错了,车再好也开不动。

这一章,我们重点聊三件事:内核镜像怎么选、DTB 怎么绑、cmdline 怎么写。这三样东西,任何一个出岔子,系统都起不来。

4.1 内核镜像格式:zImage vs Image.gz

先说说内核镜像本身。你从编译链拿到的内核,通常有两种形态:zImage 和 Image.gz。很多人搞不清区别,其实没那么玄乎。

特性 zImage Image.gz
本质 自解压镜像,头部自带解压代码 纯 gzip 压缩的 ELF 镜像
加载地址 通常固定(如 0x30008000) 由 bootloader 决定
解压方式 CPU 执行头部代码自行解压 需要 bootloader 或硬件解压
适用场景 老旧 bootloader、无解压支持 现代 bootloader(如 U-Boot)

我个人习惯,只要 bootloader 支持,优先用 Image.gz。为什么?因为 zImage 的加载地址是硬编码的,一旦你换了内存布局,就得重新编译内核。而 Image.gz 是位置无关的,bootloader 可以把它放到任意地址,灵活性高得多。

小技巧: 如果你不确定当前平台用哪种,去查 bootloader 的文档。U-Boot 通常用 bootm 命令加载 Image.gz,而老旧版本才需要 zImage。

4.2 DTB 打包:分离还是绑定?

DTB(Device Tree Blob)是内核的硬件描述文件。没有它,内核不知道你的板子上有哪些外设、内存多大、GPIO 怎么接。DTB 的打包方式,直接决定了你的系统能不能适配不同硬件。

这里有两种主流做法:

  • 分离式: DTB 单独作为一个分区,或者由 bootloader 从文件系统加载。
  • 绑定式: DTB 直接附加到内核镜像尾部,打包成一个文件。

我在项目中遇到过一种情况:同一套内核要跑在三个硬件版本上,每个版本的 DTB 都不一样。如果采用绑定式,就得为每个版本单独编译内核,维护成本极高。后来我们改用分离式,bootloader 根据硬件版本号动态加载对应的 DTB,问题就解决了。

但绑定式也有它的优势——简单、可靠。对于量产产品,硬件固定不变,绑定式可以减少一个分区,降低出错概率。

打包命令示例(绑定式):
# 将 DTB 附加到 Image.gz 尾部
cat Image.gz > kernel_with_dtb
cat board.dtb >> kernel_with_dtb

# 或者用 U-Boot 的 mkimage 工具
mkimage -A arm -O linux -T kernel -C gzip \
  -a 0x80008000 -e 0x80008000 \
  -n "Linux Kernel with DTB" \
  -d kernel_with_dtb uImage
注意: 绑定 DTB 时,必须确保 DTB 的起始地址是 4 字节对齐的。否则内核在解析时会直接崩溃。我曾经因为这个坑,排查了整整两天。

4.3 内核启动参数与 cmdline

cmdline 是内核启动时的一串参数,告诉内核根分区在哪、控制台用哪个串口、内存大小等等。它通常由 bootloader 传递给内核,但也可以硬编码在内核配置中。

一个典型的 cmdline 长这样:

console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait rw

每个参数的含义:

  • console=ttyS0,115200:指定串口控制台,波特率 115200。
  • root=/dev/mmcblk0p2:根文件系统在 eMMC 的第二个分区。
  • rootwait:等待根设备就绪再挂载(对 SD 卡/eMMC 很重要)。
  • rw:以读写方式挂载根文件系统。

你想想看,如果 root 参数写错了,内核启动时找不到根分区,就会 panic。我见过一个同事,把 mmcblk0p2 写成了 mmcblk1p2,结果板子死活起不来,最后发现是 SD 卡和 eMMC 的设备号搞混了。

4.4 打包流程与校验

好了,现在我们把内核镜像、DTB、cmdline 都准备好了。接下来就是打包。我建议的流程如下:

  1. 编译内核,生成 Image.gz。
  2. 编译 DTB,生成 board.dtb。
  3. 将 DTB 附加到内核尾部(如果采用绑定式)。
  4. 用 mkimage 或 mksquashfs 打包成 U-Boot 可识别的格式。
  5. 计算整个分区的哈希值,写入校验文件。

校验这一步,很多人会忽略。但你想,如果烧录过程中数据损坏,内核镜像少了一个字节,系统就起不来了。所以,我每次打包都会生成一个 SHA256 校验文件,烧录后由 bootloader 或上位机工具验证。

校验命令示例:
# 生成校验文件
sha256sum kernel_partition.bin > kernel_partition.sha256

# 烧录后验证
sha256sum -c kernel_partition.sha256

4.5 知识体系图

下面这张图,把 Kernel 分区打包的核心逻辑串起来了。你可以把它当作一个检查清单,每次打包时对照着走一遍。

Kernel 分区打包核心流程 内核镜像 zImage / Image.gz 设备树 DTB board.dtb 启动参数 cmdline console=... root=... 打包成分区镜像 mkimage / cat 合并 生成校验文件 SHA256 / CRC32 kernel_partition.bin 分离式:DTB 单独分区 绑定式:DTB 附加到内核尾部 cmdline 由 bootloader 动态生成或硬编码

4.6 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • DTB 版本不匹配: 内核版本和 DTB 版本必须对应。我曾经把 5.10 内核的 DTB 用在 5.4 内核上,结果 GPIO 驱动全部失效。
  • cmdline 过长: 内核 cmdline 有长度限制(通常是 2048 字节)。如果你塞了太多参数,后面的会被截断。我建议用 cat /proc/cmdline 检查实际生效的参数。
  • 打包顺序: 绑定 DTB 时,一定要先放内核镜像,再追加 DTB。顺序反了,内核会尝试把 DTB 当成代码执行,直接崩溃。
  • 校验文件丢失: 生成校验文件后,记得和镜像一起归档。我见过有人只存了镜像,校验文件丢了,烧录后出了问题也没法验证。

嗯,Kernel 分区打包这部分,说复杂也复杂,说简单也简单。核心就是三件事:选对镜像、绑好 DTB、写对 cmdline。每一步都做扎实了,系统启动就不会出大问题。