3. 打包流程入门:固件打包基本流程、打包工具链介绍(mkimage、fit_image)、参数注入时机
好,咱们进入第三章。这一章我打算把固件打包这件事从头到尾捋一遍。很多刚入行的朋友觉得打包就是「把几个二进制文件拼在一起」,嗯,这么说也没错,但实际做起来坑不少。尤其是启动参数的注入时机,搞错了,板子就起不来。
我个人习惯把打包流程分成三个阶段:准备阶段、组装阶段、校验阶段。咱们一个一个来看。
3.1 固件打包的基本流程
先看一张总图,把整个流程串起来。你想想看,一个嵌入式设备从编译完成到最终烧录,中间到底发生了什么?
这张图我画得比较简洁,但核心逻辑都在里面了。下面我展开讲讲每个阶段具体做什么。
3.1.1 准备阶段
这个阶段说白了就是「把原材料备齐」。你需要准备的东西包括:
- 内核镜像:通常是编译好的 zImage 或 Image 文件
- 设备树文件:.dtb 文件,描述硬件拓扑
- 根文件系统:可以是 initramfs、squashfs 或 ext4 镜像
- 启动参数:比如 console=ttyS0,115200、root=/dev/mmcblk0p2 这些
- Bootloader 配置:比如 U-Boot 的环境变量脚本
我在项目中遇到过一个问题:同事把设备树文件拿错了版本,结果板子上的网卡死活不工作。查了两天才发现是 DTB 和内核版本不匹配。所以准备阶段一定要做好版本对齐。
3.1.2 组装阶段
这是打包的核心环节。工具链会把上面那些原材料按照一定的格式「捏」成一个完整的固件文件。常用的格式有:
| 格式 | 工具 | 适用场景 |
|---|---|---|
| uImage | mkimage | 传统 U-Boot 启动 |
| FIT image | mkimage + .its 描述文件 | 现代 U-Boot,支持多组件和校验 |
| raw binary | dd / cat | 简单场景,直接拼接 |
| sdcard image | genimage | 完整 SD 卡/ eMMC 镜像 |
我个人最常用的是 FIT image 格式。为什么?因为它把内核、DTB、文件系统、启动参数都打包在一个文件里,而且支持哈希校验和签名。你想想看,如果每个组件单独烧录,烧录工具要处理多个文件,很容易搞错顺序或漏掉某个文件。FIT image 一个文件搞定,省心很多。
3.1.3 校验阶段
组装完成后,工具链会对固件做完整性检查。这一步不能省。我曾经见过一个案例:打包脚本在拷贝文件时发生了截断,固件少了一半,但因为没有校验,直接烧录到产线上,结果 100 块板子全部变砖。后来返工花了一周时间。
注意:校验阶段只做检查,不做修改。启动参数如果在校验阶段才注入,那就晚了——因为校验值已经算好了,你改了内容,校验就会失败。
3.2 打包工具链介绍
工具链这块,咱们重点讲两个:mkimage 和 FIT image。这两个是 U-Boot 生态里最核心的打包工具。
3.2.1 mkimage 基础用法
mkimage 是 U-Boot 提供的命令行工具。它的基本用法是这样的:
# 传统 uImage 格式
mkimage -A arm -O linux -T kernel -C none \
-a 0x80008000 -e 0x80008000 \
-n "Linux 5.10 kernel" \
-d zImage uImage
# 参数说明:
# -A 架构(arm, arm64, riscv 等)
# -O 操作系统(linux, barebox 等)
# -T 镜像类型(kernel, ramdisk, fdt 等)
# -C 压缩类型(none, gzip, lzma 等)
# -a 加载地址
# -e 入口地址
# -n 镜像名称(描述信息)
# -d 输入文件
# 最后一个参数是输出文件
嗯,这里要注意:-a 和 -e 这两个地址非常关键。加载地址告诉 U-Boot 把内核放到内存的哪个位置,入口地址告诉 U-Boot 从哪里开始执行。如果这两个地址搞错了,内核要么起不来,要么跑飞。
小技巧:我习惯把加载地址和入口地址设成一样的。这样 U-Boot 把内核加载到内存后,直接跳转到同一个地址执行,省去了一次地址转换。当然,如果你的内核有自解压逻辑,入口地址可能需要偏移,具体看内核的链接脚本。
3.2.2 FIT image 详解
FIT image 是 Flattened Image Tree 的缩写。它使用一个 .its 描述文件(Image Tree Source)来定义固件的组成结构。看一个例子:
// boot.its - FIT image 描述文件
/dts-v1/;
/ {
description = "FIT image for my board";
#address-cells = <1>;
images {
kernel {
description = "Linux kernel";
data = /incbin/("./zImage");
type = "kernel";
arch = "arm";
os = "linux";
compression = "none";
load = <0x80008000>;
entry = <0x80008000>;
hash-1 {
algo = "sha256";
};
};
fdt {
description = "Device tree";
data = /incbin/("./board.dtb");
type = "flat_dt";
arch = "arm";
compression = "none";
hash-1 {
algo = "sha256";
};
};
ramdisk {
description = "initramfs";
data = /incbin/("./initramfs.cpio.gz");
type = "ramdisk";
arch = "arm";
os = "linux";
compression = "gzip";
hash-1 {
algo = "sha256";
};
};
};
configurations {
default = "config-1";
config-1 {
description = "Boot with kernel + fdt + ramdisk";
kernel = "kernel";
fdt = "fdt";
ramdisk = "ramdisk";
hash-1 {
algo = "sha256";
};
};
};
};
然后用 mkimage 编译这个描述文件:
mkimage -f boot.its boot.itb
输出文件 boot.itb 就是一个完整的 FIT image,里面包含了内核、设备树、文件系统以及它们的哈希值。U-Boot 在启动时会解析这个文件,校验每个组件的完整性,然后按配置加载执行。
为什么我推荐 FIT image?因为它把「启动参数」也纳入了管理。你可以在 .its 文件里通过 bootargs 属性指定启动参数,或者通过 U-Boot 的环境变量动态注入。这样参数和固件是绑定的,不会出现「固件烧对了,但参数是旧的」这种尴尬情况。
3.3 参数注入时机
好,终于到了本章最关键的部分。启动参数到底什么时候注入?我总结了三个时机:
| 注入时机 | 方法 | 优点 | 缺点 |
|---|---|---|---|
| 编译时 | 在内核配置中写死 CONFIG_CMDLINE | 简单,参数固定不变 | 不灵活,换参数要重新编译内核 |
| 打包时 | 在 .its 文件中指定 bootargs,或通过 mkimage 注入 | 参数与固件绑定,版本可控 | 需要修改 .its 文件并重新打包 |
| 启动时 | U-Boot 环境变量 bootargs,或通过 bootcmd 动态拼接 | 最灵活,可以在产线或现场调整 | 依赖 U-Boot 环境,容易被误修改 |
我个人建议:优先使用打包时注入。为什么?
- 编译时注入太死板,不适合产品迭代
- 启动时注入太灵活,容易出人为错误
- 打包时注入刚好居中——参数和固件版本绑定,但修改参数只需要改 .its 文件,不需要重新编译内核
我在项目中遇到过这样一个坑:产线上用 U-Boot 环境变量传参,结果某个批次的产品 flash 有坏块,U-Boot 环境变量读出来是乱码,启动参数变成了 console=ttyS0,115200 后面跟了一堆乱码字符,导致内核串口驱动初始化失败。从那以后,我坚持把关键参数打包进固件,U-Boot 环境变量只用来传一些非关键信息(比如设备序列号)。
核心原则:启动参数的注入时机,决定了参数的「所有权」。编译时注入,所有权在内核开发者;打包时注入,所有权在固件发布者;启动时注入,所有权在产线或现场工程师。你想让谁控制参数,就在哪个阶段注入。
3.4 实战:一个完整的打包脚本
最后,我分享一个我实际项目中用过的打包脚本片段。它展示了从编译输出到最终 FIT image 的完整流程:
#!/bin/bash
# build_fit.sh - 固件打包脚本
set -e
# 1. 准备阶段:收集编译产物
KERNEL="output/zImage"
DTB="output/board.dtb"
RAMDISK="output/initramfs.cpio.gz"
ITS_TEMPLATE="boot_template.its"
ITS_OUTPUT="build/boot.its"
ITB_OUTPUT="build/boot.itb"
# 2. 参数注入:生成 .its 文件
# 从配置文件中读取启动参数
BOOTARGS=$(cat config/bootargs.txt)
echo "Bootargs: $BOOTARGS"
# 替换模板中的占位符
sed "s|__BOOTARGS__|$BOOTARGS|g" $ITS_TEMPLATE > $ITS_OUTPUT
# 3. 组装阶段:生成 FIT image
mkimage -f $ITS_OUTPUT $ITB_OUTPUT
# 4. 校验阶段:检查哈希
echo "Verifying FIT image..."
dumpimage -l $ITB_OUTPUT | grep -q "sha256" || {
echo "ERROR: Hash check failed!"
exit 1
}
echo "FIT image generated: $ITB_OUTPUT"
echo "Size: $(stat -c%s $ITB_OUTPUT) bytes"
这个脚本的核心逻辑就是:从配置文件读取启动参数,注入到 .its 模板中,然后生成 FIT image。每次发布固件时,只需要修改 config/bootargs.txt 文件,不需要动脚本本身。
经验之谈:我建议把 .its 文件也纳入版本管理。这样你可以回溯任何一个固件版本用了哪些参数。我曾经靠这个功能,快速定位了一个「某批次设备无法联网」的问题——对比后发现是参数文件里把 ethaddr 写错了。