3、差分包生成流程:源版本提取、目标版本提取、差异计算、补丁打包
好,咱们进入正题。差分包怎么来的?说白了就四步:源版本提取 → 目标版本提取 → 差异计算 → 补丁打包。每一步都有坑,我一个个讲。
3.1 源版本提取:你得知道「从哪来」
源版本,就是设备当前正在跑的固件。提取它的目的是给差异计算工具当「旧版参照物」。
我个人习惯,源版本提取分两种情况:
- 本地构建时保留:编译新固件时,把上一版固件镜像归档到
release/目录。这样差分包生成脚本直接读文件,不用去设备上拉。 - 从设备回读:如果没保留旧镜像,就得从设备 flash 里把当前固件读出来。我遇到过一种情况——设备 flash 有坏块,读出来的数据跟原始镜像不一致,差异计算直接崩了。
提取时,我一般用 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,它对二进制文件的压缩率很漂亮。
差异计算的原理,我简单说一下:
- 把两个文件切成固定大小的块(比如 64KB)。
- 用滚动哈希(Rabin-Karp 或类似算法)找匹配块。
- 对不匹配的块,记录「新增」或「修改」的字节序列。
- 输出一个差异文件(patch),里面包含控制指令和数据。
举个例子,用 bsdiff 生成差分包:
bsdiff old_firmware.bin new_firmware.bin upgrade.patch
这条命令跑完后,upgrade.patch 就是差分包。我见过一个实际案例:源版本 8MB,目标版本 8.2MB,生成的差分包只有 300KB。为什么?因为两个版本只改了配置文件和几个驱动模块,90% 的代码没动。
但差异计算也有翻车的时候。我曾经遇到一个坑:源版本和目标版本的编译时间戳不同,导致整个固件头部都变了。差异计算工具把整个文件当成「全新数据」,差分包体积直接飙到 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 图,把整个差分包生成流程串起来了。你一看就明白。
3.6 避坑指南
做差分升级这几年,我踩过的坑不少。挑几个典型的说说:
- 源版本不干净:设备 flash 里有残留数据,读出来的固件比原始镜像多了几个字节。差异计算时,这些「脏数据」被当成差异,补丁包体积暴增。解决方案:读取时按分区大小精确截断。
- 编译选项不一致:源版本用 GCC 9.3 编译,目标版本用 GCC 10.2 编译。同样的代码,生成的二进制差异巨大。我后来强制统一编译工具链版本。
- 文件系统差异:如果固件里包含文件系统(如 squashfs、jffs2),不同版本的 mkfs 工具可能生成不同的元数据。我建议在差异计算前,先对文件系统解包,只比较文件内容,不比较元数据。
好了,差分包生成流程就这些。记住四个步骤:提取源、提取目标、算差异、打包。每一步都有细节,但核心是差异计算那一步——它决定了你的升级包是 300KB 还是 8MB。