Skip to content

为什么需要完整镜像 - 别再手工拼系统了

前言:我们为什么又要折腾一个 .img

说实话,刚开始做板子启动的时候,我其实并不觉得“完整镜像”有什么必要。

U-Boot 有了,内核有了,设备树也有了,rootfs 目录也在那儿。那不就是把 u-boot-dtb.imx 写到固定偏移,再把 zImage.dtb 拷到 boot 分区,最后把 rootfs 拷到 root 分区吗?

听起来很简单,对吧。

问题是,这种“手工拼系统”的方式只要步骤一多,就会开始露出它最麻烦的一面:每一步都看起来合理,但任何一步做错,最后表现出来都可能只是板子起不来。

你可能会遇到这些情况:

  • U-Boot 烧进去了,但分区表没有按预期创建
  • zImage 和 DTB 拷过去了,但 rootfs 没有同步完整
  • SD 卡镜像用了 eMMC 的 root 参数
  • 只更新了 boot 分区,却忘了 raw U-Boot 还停留在旧版本
  • 复制文件的时候看起来成功了,实际权限、软链接或设备节点已经变味

这些问题排查起来很烦,因为它们不是“编译失败”这种明确错误,而是启动链条中某个环节悄悄不对。完整镜像要解决的,就是这个问题。

我们现在手里有什么

先把构建产物摆出来看一眼。一次 release 构建完成后,out/release-latest/ 下面大概是这个样子:

text
out/release-latest/
├── uboot/
│   └── u-boot-dtb.imx
├── linux/
│   └── arch/arm/boot/
│       ├── zImage
│       └── dts/nxp/imx/imx6ull-aes.dtb
└── rootfs/
    ├── bin/
    ├── etc/
    ├── lib/
    └── ...

这几个东西都很重要,但它们还只是“零件”。

u-boot-dtb.imx 是启动第一个阶段要用的东西;zImage 是内核;.dtb 告诉内核板子长什么样;rootfs/ 是 Linux 起来之后真正进入的用户空间。

这些零件单独看都没问题,但板子不会因为你把零件放在一个目录里就自动启动。板子需要的是一块存储设备上的完整布局。

也就是说,我们真正想要的是这样一个东西:

text
一整块“假想的 SD/eMMC”
├── 开头的 raw U-Boot 区域
├── 分区表
├── boot 分区
│   ├── zImage
│   └── imx6ull-aes.dtb
└── rootfs 分区
    └── 完整根文件系统

这个“假想的 SD/eMMC”就是 .img 文件。

手工拷贝为什么不够稳

手工拷贝最大的问题不是它不能用,而是它太依赖“人脑状态”。

比如你今天只改了设备树,很自然会想着:“那我只拷一个 DTB 就行。”这在开发阶段没问题。可过两天你又改了 U-Boot 默认环境,再过两天 rootfs overlay 也变了。等到要交付或者复现问题的时候,你很可能已经忘了哪一步是最新的。

更麻烦的是,手工流程里有些东西不是文件复制能覆盖的。

U-Boot 就是典型例子。它不是 boot 分区里的一个普通文件,而是写在存储设备前面某个 raw 偏移上的启动镜像。你把 zImage 拷得再漂亮,也不会顺便更新 raw U-Boot。

分区表也是一样。你在文件管理器里能看到 boot 分区和 rootfs 分区,不代表分区布局就是项目当前期望的布局。

所以完整镜像的价值不只是“少敲几条命令”,而是把整套启动状态固化下来。

完整镜像到底固定了什么

build_imx6ull_image.sh 生成的镜像,本质上把四件事固定在一个文件里。

第一件事是 U-Boot 的位置。脚本会把:

text
out/release-latest/uboot/u-boot-dtb.imx

写到镜像文件的 1 KiB 偏移处。这个位置不是某个分区内部,而是 raw disk 区域。

第二件事是分区表。镜像里会有一个 MBR 分区表,告诉系统哪里是 boot 分区,哪里是 rootfs 分区。

第三件事是 boot 分区内容。这里面会放:

text
/zImage
/imx6ull-aes.dtb
/boot.cmd

脚本也会额外放一份到 /boot/ 目录,方便后续兼容不同启动习惯。

第四件事是 rootfs。out/release-latest/rootfs/ 会被打包成第二个 ext4 分区。Linux 启动后挂载的根目录,就是这里。

这样一来,最终产物就很明确:

text
out/release-latest/images/imx6ull-aes-emmc.img
out/release-latest/images/imx6ull-aes-sd.img

你拿到这个文件,不需要再猜“这个镜像是不是少了 rootfs”“这个 U-Boot 是不是旧的”。它就是一整块盘。

SD 和 eMMC 为什么不能混着来

这里有个很容易踩的坑:SD 镜像和 eMMC 镜像看起来非常像,但它们不能随便混用。

原因不在文件系统内容,而在启动参数。

在当前项目约定里:

目标介质U-Boot 里看到的设备Linux 里的 root 分区
SDmmc 0/dev/mmcblk0p2
eMMCmmc 1/dev/mmcblk1p2

如果你用 eMMC 镜像去走 SD 启动,U-Boot 命令里可能会去 mmc 1:1 加载内核,Linux 也会去找 /dev/mmcblk1p2。如果板子当前实际从 SD 起,那这个 root 设备很可能根本不是你想要的那个。

这类问题在串口里通常会变成一句很朴素的报错:

text
VFS: Cannot open root device

所以脚本提供了:

bash
--boot-media=sd
--boot-media=emmc

它不只是改了输出文件名。它会影响 boot.cmd、manifest 里的启动命令,以及 Linux 的 root= 参数。

完整镜像和 NFS 不是一回事

这里还要顺手澄清一下:完整镜像不是为了替代 NFS。

NFS 在开发阶段非常舒服。你改主机上的 rootfs,板子重启后就能看到变化,不需要反复烧卡。做驱动调试、应用调试、rootfs 配置验证时,NFS 仍然是很重要的工具。

完整镜像更适合这些场景:

  • 第一次把板子启动起来
  • 做 SD/eMMC 的完整启动验证
  • 给别人一个可复现的系统产物
  • release 构建后确认交付物完整
  • 排查“到底是文件没拷对,还是启动参数不对”

你可以把它理解成两种工作模式:

text
开发迭代:TFTP + NFS
交付验证:完整 SD/eMMC 镜像

一个负责快,一个负责稳。

我们接下来要看什么

这一章先解决“为什么要做完整镜像”的问题。下一章我们拆开这个 .img,看看里面每个偏移、每个分区为什么这样安排。

你会发现,脚本并没有做什么神秘的事情。它只是把我们原来手工做的步骤,变成了一套固定、可检查、可复现的流程。

下一步: 继续阅读 06_image_layout_design.md,看完整镜像内部到底长什么样。

Built with VitePress