3. 打包流程入门:固件打包基本流程、打包工具链介绍(mkimage、fit_image)、参数注入时机

好,咱们进入第三章。这一章我打算把固件打包这件事从头到尾捋一遍。很多刚入行的朋友觉得打包就是「把几个二进制文件拼在一起」,嗯,这么说也没错,但实际做起来坑不少。尤其是启动参数的注入时机,搞错了,板子就起不来。

我个人习惯把打包流程分成三个阶段:准备阶段、组装阶段、校验阶段。咱们一个一个来看。

3.1 固件打包的基本流程

先看一张总图,把整个流程串起来。你想想看,一个嵌入式设备从编译完成到最终烧录,中间到底发生了什么?

固件打包基本流程 ① 准备阶段 编译输出(ELF/BIN) 设备树(DTB) 启动参数配置文件 ② 组装阶段 mkimage 打包 生成 FIT image 注入启动参数 ③ 校验阶段 CRC/哈希校验 签名验证 生成烧录文件 关键点:启动参数必须在组装阶段注入, 校验阶段只做完整性检查,不再修改内容

这张图我画得比较简洁,但核心逻辑都在里面了。下面我展开讲讲每个阶段具体做什么。

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 写错了。