3、差分包生成流程:源版本提取、目标版本提取、差异计算、补丁打包

好,咱们进入正题。差分包怎么来的?说白了就四步:源版本提取 → 目标版本提取 → 差异计算 → 补丁打包。每一步都有坑,我一个个讲。

3.1 源版本提取:你得知道「从哪来」

源版本,就是设备当前正在跑的固件。提取它的目的是给差异计算工具当「旧版参照物」。

我个人习惯,源版本提取分两种情况:

  • 本地构建时保留:编译新固件时,把上一版固件镜像归档到 release/ 目录。这样差分包生成脚本直接读文件,不用去设备上拉。
  • 从设备回读:如果没保留旧镜像,就得从设备 flash 里把当前固件读出来。我遇到过一种情况——设备 flash 有坏块,读出来的数据跟原始镜像不一致,差异计算直接崩了。
注意:源版本必须与设备当前固件完全一致,包括 bootloader、分区表、文件系统元数据。差一个字节,补丁打上去就可能变砖。

提取时,我一般用 dd 或厂家提供的 flash 读取工具,指定起始地址和长度。举个例子:

# 从 NAND flash 偏移 0x200000 处读取 8MB 固件
dd if=/dev/mtd5 of=source_v1.2.bin bs=1k count=8192 skip=2048

嗯,这里要注意:skip 参数的单位要和 bs 匹配,不然读出来的数据全是错的。我曾经因为这个排查了整整一下午。

3.2 目标版本提取:你得知道「到哪去」

目标版本就是你要升级上去的新固件。这个相对简单,编译产物直接拿过来用。

但有个细节容易被忽略——目标版本必须和源版本同构。什么意思?就是两个固件必须基于同一套硬件抽象层、同一套分区布局。如果目标版本改了分区大小,那差分包生成时就会算出大量「假差异」,补丁包体积反而比全量包还大。

对比项 源版本 目标版本
来源 设备回读 / 构建归档 新编译产物
完整性要求 必须与设备一致 必须与发布包一致
常见问题 坏块、读取偏移错误 编译选项不同导致二进制差异

3.3 差异计算:核心中的核心

差异计算,就是把源版本和目标版本逐字节对比,找出「哪些地方变了」。常用的工具有 bsdiff、xdelta3、hdiffpatch。我项目里用得最多的是 bsdiff,它对二进制文件的压缩率很漂亮。

差异计算的原理,我简单说一下:

  1. 把两个文件切成固定大小的块(比如 64KB)。
  2. 用滚动哈希(Rabin-Karp 或类似算法)找匹配块。
  3. 对不匹配的块,记录「新增」或「修改」的字节序列。
  4. 输出一个差异文件(patch),里面包含控制指令和数据。

举个例子,用 bsdiff 生成差分包:

bsdiff old_firmware.bin new_firmware.bin upgrade.patch

这条命令跑完后,upgrade.patch 就是差分包。我见过一个实际案例:源版本 8MB,目标版本 8.2MB,生成的差分包只有 300KB。为什么?因为两个版本只改了配置文件和几个驱动模块,90% 的代码没动。

关键指标:差分包体积 / 全量包体积 的比值。我一般要求这个值 < 30%,否则差分升级的意义就不大了。

但差异计算也有翻车的时候。我曾经遇到一个坑:源版本和目标版本的编译时间戳不同,导致整个固件头部都变了。差异计算工具把整个文件当成「全新数据」,差分包体积直接飙到 7.8MB(全量包才 8MB)。后来我强制在编译时固定时间戳,才解决这个问题。

3.4 补丁打包:把差异装进「容器」

差异计算完,得到的是原始 patch 文件。但直接拿它去升级,不够安全。我们需要把它打包成标准格式,加上校验信息。

我常用的打包结构是这样的:

差分升级包结构:
├── 包头(32 字节)
│   ├── 魔数(4B):0xDF55 4D50
│   ├── 版本号(4B):源版本 + 目标版本
│   ├── 包体长度(4B):patch 数据长度
│   ├── 校验算法(1B):0x01=CRC32, 0x02=SHA256
│   └── 保留字段(19B)
├── 包体(可变长度)
│   └── patch 数据(bsdiff/xdelta3 输出)
└── 包尾(4B)
    └── CRC32 校验值

打包脚本我一般用 Python 写,简单可控:

import struct, hashlib

def pack_delta(source_ver, target_ver, patch_data):
    magic = b'\xDF\x55\x4D\x50'
    version = struct.pack('II', source_ver, target_ver)
    body_len = len(patch_data)
    algo = b'\x02'  # SHA256
    reserved = b'\x00' * 19
    header = magic + version + struct.pack('I', body_len) + algo + reserved
    body = patch_data
    crc = struct.pack('I', zlib.crc32(body) & 0xFFFFFFFF)
    return header + body + crc
小技巧:我习惯在包头里同时记录源版本和目标版本。这样设备端在应用补丁前,可以先校验当前版本是否匹配,避免打错补丁。

3.5 整体流程:一张图说清楚

下面这张 SVG 图,把整个差分包生成流程串起来了。你一看就明白。

差分包生成流程 差分包生成流程 源版本提取 设备回读 / 构建归档 目标版本提取 新编译产物 差异计算 bsdiff / xdelta3 / hdiffpatch 补丁打包 包头 + 包体 + CRC/SHA256 差分包 .patch ⚠ 关键检查点 源/目标版本同构 编译时间戳固定

3.6 避坑指南

做差分升级这几年,我踩过的坑不少。挑几个典型的说说:

  • 源版本不干净:设备 flash 里有残留数据,读出来的固件比原始镜像多了几个字节。差异计算时,这些「脏数据」被当成差异,补丁包体积暴增。解决方案:读取时按分区大小精确截断。
  • 编译选项不一致:源版本用 GCC 9.3 编译,目标版本用 GCC 10.2 编译。同样的代码,生成的二进制差异巨大。我后来强制统一编译工具链版本。
  • 文件系统差异:如果固件里包含文件系统(如 squashfs、jffs2),不同版本的 mkfs 工具可能生成不同的元数据。我建议在差异计算前,先对文件系统解包,只比较文件内容,不比较元数据。
重要:差分包生成后,一定要做回环验证——用源版本 + 差分包,通过补丁应用工具还原出目标版本,然后跟原始目标版本做逐字节对比。不一致的话,这个差分包绝对不能发布。

好了,差分包生成流程就这些。记住四个步骤:提取源、提取目标、算差异、打包。每一步都有细节,但核心是差异计算那一步——它决定了你的升级包是 300KB 还是 8MB。