3. 升级包制作:差分升级算法与全量包流程

好,咱们进入第三章。这一章讲的是升级包怎么造出来的。

说实话,很多工程师觉得升级包制作就是打个压缩包。其实不然。你想想看,一个设备可能只有几兆的存储空间,用户下载一个几百兆的全量包,体验能好吗?所以,差分升级才是嵌入式设备的灵魂。

3.1 差分升级算法:bsdiff vs hdiff

差分升级的核心思想很简单——只传输新旧版本之间的差异部分。但怎么高效地找出差异,这就是算法的事了。

bsdiff 是业界最经典的差分算法。它基于后缀排序,能生成非常紧凑的补丁包。我在三星的项目中,用 bsdiff 给一个 128MB 的固件做差分,补丁包只有 15MB 左右。压缩比相当可观。

但 bsdiff 有个缺点——吃内存。旧版本和新版本都要加载到内存里,对于资源受限的嵌入式设备,这可能是个问题。

hdiff 是另一种选择。它基于哈希匹配,速度更快,内存占用也更低。我个人的习惯是:如果设备内存小于 64MB,优先考虑 hdiff;如果内存充裕,bsdiff 的压缩率更香。

实际项目中的选择建议:

  • bsdiff:压缩率最优,适合服务器端生成补丁,设备端内存充足
  • hdiff:速度快、内存低,适合低端 MCU 或 IoT 设备
  • xdelta:基于 VCDIFF 标准,兼容性较好,但压缩率一般

我曾经在一个智能门锁项目里踩过坑。当时用了 bsdiff,结果设备只有 32MB 内存,差分时直接 OOM 了。后来换成 hdiff,问题解决。嗯,这里要注意——算法选型一定要结合硬件资源。

3.2 全量升级包制作流程

全量升级包听起来简单,就是把整个固件打包。但流程中每一步都有讲究。

我一般把全量包制作分成四个阶段:

  1. 固件编译:生成最终的二进制文件,比如 .bin 或 .hex
  2. 元数据生成:写入版本号、设备型号、校验值、签名等信息
  3. 打包压缩:用 gzip 或 lzma 压缩,减少传输体积
  4. 签名封装:对包体进行数字签名,防止篡改

下面是一个典型的全量包结构:

+-------------------------------+
|       包头 (Header)           |
|  - 魔数 (Magic Number)        |
|  - 版本号 (Version)           |
|  - 设备型号 (Model)           |
|  - 固件大小 (Size)            |
|  - 校验值 (CRC32/SHA256)      |
+-------------------------------+
|       固件数据 (Payload)       |
|  - 压缩后的二进制固件          |
+-------------------------------+
|       签名区 (Signature)       |
|  - RSA/ECDSA 签名值            |
+-------------------------------+

你可能会问,为什么要有魔数?说白了,就是防止设备刷错了包。我曾经见过一个案例,有人把路由器的固件刷到了摄像头里,结果变砖了。魔数就是第一道防线。

3.3 签名与校验

签名是升级包安全的最后一道关。没有签名的升级包,就像没锁的门——谁都能进。

我推荐的做法是:

  • 签名算法:使用 ECDSA 或 RSA-2048 以上。我个人偏向 ECDSA,因为密钥更短,计算更快
  • 哈希算法:SHA-256 起步,别再用 MD5 或 SHA-1 了,已经被证明不安全
  • 校验流程:设备端先验签,再解包,最后刷写。顺序不能乱

避坑指南:

我曾经在一个项目里,把签名校验放在了刷写之后。结果有人伪造了一个升级包,设备刷完才发现签名不对,但系统已经半残了。从那以后,我坚持「先验签,后刷写」的原则。

签名流程的伪代码大致如下:

// 服务端签名
1. 计算固件哈希: hash = SHA256(firmware_data)
2. 用私钥签名: signature = ECDSA_sign(private_key, hash)
3. 打包: package = header + firmware_data + signature

// 设备端校验
1. 解析包头,获取固件大小和签名值
2. 计算固件哈希: hash = SHA256(firmware_data)
3. 用公钥验签: valid = ECDSA_verify(public_key, hash, signature)
4. 如果 valid == false,直接拒绝升级
5. 如果 valid == true,继续刷写流程

重要提醒:

公钥一定要固化在设备的只读存储区(如 OTP 或 eFuse)。如果公钥可以被篡改,签名就形同虚设。我见过有厂商把公钥放在普通 Flash 里,结果被黑客替换了公钥,整个升级体系直接崩溃。

3.4 差分包的制作与校验

差分包的制作比全量包多一步——生成差异数据。流程如下:

  1. 获取旧版本固件(base version)和新版本固件(target version)
  2. 运行 bsdiff 或 hdiff,生成 patch 文件
  3. 将 patch 文件与元数据打包
  4. 对整个包进行签名

设备端收到差分包后,需要先验签,然后用旧固件 + patch 还原出新固件。还原过程也要做校验,防止还原出错。

我习惯在差分包里额外放一个「还原后的固件哈希」。设备还原完成后,计算一次哈希,跟包里的值比对。如果对不上,说明还原过程出了问题,需要回滚。

嗯,这一章的内容就这些。差分算法选型、全量包结构、签名校验流程,都是实战中必须掌握的。下一章我们聊聊升级过程中的异常处理和回滚机制——那才是真正考验系统设计能力的地方。