为什么需要完整镜像 - 别再手工拼系统了
前言:我们为什么又要折腾一个 .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/ 下面大概是这个样子:
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 起来之后真正进入的用户空间。
这些零件单独看都没问题,但板子不会因为你把零件放在一个目录里就自动启动。板子需要的是一块存储设备上的完整布局。
也就是说,我们真正想要的是这样一个东西:
一整块“假想的 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 的位置。脚本会把:
out/release-latest/uboot/u-boot-dtb.imx写到镜像文件的 1 KiB 偏移处。这个位置不是某个分区内部,而是 raw disk 区域。
第二件事是分区表。镜像里会有一个 MBR 分区表,告诉系统哪里是 boot 分区,哪里是 rootfs 分区。
第三件事是 boot 分区内容。这里面会放:
/zImage
/imx6ull-aes.dtb
/boot.cmd脚本也会额外放一份到 /boot/ 目录,方便后续兼容不同启动习惯。
第四件事是 rootfs。out/release-latest/rootfs/ 会被打包成第二个 ext4 分区。Linux 启动后挂载的根目录,就是这里。
这样一来,最终产物就很明确:
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 分区 |
|---|---|---|
| SD | mmc 0 | /dev/mmcblk0p2 |
| eMMC | mmc 1 | /dev/mmcblk1p2 |
如果你用 eMMC 镜像去走 SD 启动,U-Boot 命令里可能会去 mmc 1:1 加载内核,Linux 也会去找 /dev/mmcblk1p2。如果板子当前实际从 SD 起,那这个 root 设备很可能根本不是你想要的那个。
这类问题在串口里通常会变成一句很朴素的报错:
VFS: Cannot open root device所以脚本提供了:
--boot-media=sd
--boot-media=emmc它不只是改了输出文件名。它会影响 boot.cmd、manifest 里的启动命令,以及 Linux 的 root= 参数。
完整镜像和 NFS 不是一回事
这里还要顺手澄清一下:完整镜像不是为了替代 NFS。
NFS 在开发阶段非常舒服。你改主机上的 rootfs,板子重启后就能看到变化,不需要反复烧卡。做驱动调试、应用调试、rootfs 配置验证时,NFS 仍然是很重要的工具。
完整镜像更适合这些场景:
- 第一次把板子启动起来
- 做 SD/eMMC 的完整启动验证
- 给别人一个可复现的系统产物
- release 构建后确认交付物完整
- 排查“到底是文件没拷对,还是启动参数不对”
你可以把它理解成两种工作模式:
开发迭代:TFTP + NFS
交付验证:完整 SD/eMMC 镜像一个负责快,一个负责稳。
我们接下来要看什么
这一章先解决“为什么要做完整镜像”的问题。下一章我们拆开这个 .img,看看里面每个偏移、每个分区为什么这样安排。
你会发现,脚本并没有做什么神秘的事情。它只是把我们原来手工做的步骤,变成了一套固定、可检查、可复现的流程。
下一步: 继续阅读 06_image_layout_design.md,看完整镜像内部到底长什么样。