news 2026/10/27 10:32:39

从零理解差分升级 JojoDiff + Uzlib:一份补丁,怎样还原成新固件?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零理解差分升级 JojoDiff + Uzlib:一份补丁,怎样还原成新固件?

从零理解差分升级 JojoDiff + Uzlib:一份补丁,怎样还原成新固件?

我们已经能在电脑上生成升级、回退差分包,也能用上位机验证还原。但真正准备移植时,仍可能卡在这些问题上:旧程序地址填在哪里?谁负责解压?为什么需要回调?函数返回后,怎样知道新程序可靠?

本文依据当前“差分升级工具”上位机的还原验证实际 C 源码,使用“内存中的旧程序、完整差分包、独立输出区域”讲清整条链路。暂不涉及 Flash 擦除、编程对齐、Bootloader 切换和断电恢复。

读完后,你应当能理解四个读写接口的作用,知道初始化、还原、结束检查和 CRC 校验的调用顺序,并能区分哪些代码属于算法、哪些属于设备适配。

本文使用本项目约定的56字节小端头部。它是应用层协议,不是 JojoDiff 或 Uzlib 的通用标准。当前包不包含通信分包大小、总包数。其他项目采用不同协议时,应先核对包格式。

一、先用“修改一本书”理解差分还原

假设旧程序是一段内容:

旧内容:ABCDEF 新内容:ABCXYZ

完整更新可以直接发送ABCXYZ;差分更新则可以表达成“保留 ABC,把后面的内容改成 XYZ”。设备持有旧内容,再执行修改说明,得到新内容。

这只是理解方式,不是 JojoDiff 实际补丁的字节编码。真正的固件是任意二进制数据,真实补丁由工具生成,不需要你手写。

这条链上各组件的分工如下:

组件在哪里用负责什么
JojoDiff电脑制包比较旧、新文件,生成原始差分补丁
压缩编码器电脑制包压缩原始补丁,降低传输量
Uzlib设备还原解开压缩,提供原始补丁字节
Janpatch设备还原解释补丁,读取旧程序,生成新程序
你编写的应用层设备还原解析包头、配置读写、组织调用和校验

**目的:先分清“谁做什么”。**设备还原不需要运行 JojoDiff.exe,也不需要 Python。它需要的是 C 源码、读写接口和正确的输入。

差分并不保证所有文件都能生成更小的升级包。文件变化程度、固件布局及压缩效果都会影响结果。本文先解决“怎样正确还原”。

二、准备三个互不重叠的数据区域

先不管具体 MCU,也不管存储器型号。只假设应用能访问三个区域:

区域A:旧程序 已存在,只读 区域B:完整差分包 已接收完整,只读 区域C:新程序输出 容量足够,用于保存结果

区域C不能覆盖区域A或区域B。还原过程仍然可能读取旧程序和补丁,过早覆盖会破坏后续输入。

需要准备的信息用途
旧程序起点、实际长度为旧程序读取接口提供边界
完整差分包起点、实际长度读取包头、验证包完整性
新程序输出起点、可用容量防止还原输出越界

这里的“起点”可以是 RAM 指针,也可以是外部存储的逻辑地址。之后讲到回调时,你会看到算法并不必直接持有真实硬件地址。

**目的:先把数据放对地方,再启动算法。**当前阶段不讨论直接覆盖旧程序。

三、差分包里装了什么?先认清三个长度


完整包的布局为:

完整差分包起点 │ ├── 0~55:56字节头部 └── 56开始:压缩后的原始补丁

当前头部定义:

字节偏移长度内容
016旧版本名称,UTF-8,未用字节补0
1616新版本名称,UTF-8,未用字节补0
324压缩有效数据大小,不含56字节头部
364旧程序大小
404新程序大小
444解压后的原始差分补丁大小
484旧程序 CRC32
524新程序 CRC32

四字节数值使用小端格式。例如0x00001234在文件中是34 12 00 00。

最需要区分的三个长度是:

名称交给谁含义
压缩有效数据大小zip.sizeUzlib 一共能读取多少压缩字节
原始补丁大小patch.patch.sizeJanpatch 一共能读取多少解压后补丁字节
新程序大小patch.target.size最终应生成多少字节

**目的:让每层知道自己的数据边界。**压缩字节的位置,不能直接等同于解压后补丁的位置。

再明确一下压缩术语:本项目使用 DEFLATE 压缩、gzip 封装,解压由 Uzlib 完成,压缩窗口限制为4 KiB。Uzlib 是库名,gzip 是封装格式,它们不是二选一的概念。

如果导入的是 HEX,上位机会先按约定转成连续字节镜像,地址空洞填0xFF。设备做旧程序 CRC 时,也必须读到相同的字节内容和相同长度,不能仅拼接 HEX 中实际出现的数据记录。

四、为什么要用回调?先理解“库向你要数据”

你无需把所有硬件驱动写进算法。只要告诉库:

当你要读旧程序时,调用这个函数;当你要写新程序时,调用另一个函数。

你注册进去的函数就是回调。你负责实现它,库负责在需要时调用它。

图中箭头表示“调用/请求”;数据通过回调返回给调用者。当前有四个连接点:

回调谁调用它你需要提供什么
read_zipUzlib 适配层指定偏移处的压缩字节
read_oldJanpatch 适配层指定偏移处的旧程序字节
read_patchJanpatch 适配层调用decompress()取得原始补丁
write_newJanpatch 适配层将生成的数据保存到输出区域

这些回调名称来自当前上位机验证封装。你的应用可以换函数名,但签名和职责要匹配。

一个容易误解的地方:不是先完整解压,再开始还原

你只需启动janpatch_cli()。它需要补丁字节时,调用read_patch(),后者再调用decompress()。Uzlib 需要更多压缩输入时,再调用read_zip()。

因此实际执行类似:

Janpatch 需要一段原始补丁 → read_patch() → decompress() → 必要时 read_zip() ← 返回解压后的补丁字节 → 根据补丁读取旧程序 → 将新程序片段交给 write_new() → 重复,直到完成

**目的:不必为完整解压补丁准备同等大小的 RAM。**但解压字典和工作缓存仍需要 RAM,后文会计算。

此处是“从已保存的完整包中按需读取”,不是“网络数据还没接收完整就开始还原”。

五、按十步走完一次还原

第1步:解析头部——先知道输入的含义

**做什么:**从完整包前56字节取出版本、长度和 CRC。

**为什么:**后续解压、还原、容量检查都需要这些信息。

**怎样做:**按字节解析小端整数,避免依赖结构体对齐和 CPU 字节序。

staticuint32_tread_le32(constuint8_t*p){return(uint32_t)p[0]|((uint32_t)p[1]<<8)|((uint32_t)p[2]<<16)|((uint32_t)p[3]<<24);}/* 调用前必须确认包至少有56字节 */payload_size=read_le32(package+32);old_size=read_le32(package+36);new_size=read_le32(package+40);patch_size=read_le32(package+44);

这里以及后文的代码是分步讲解片段,不能直接拼接为完整可运行程序。已有完整流程请查看项目FotaDesktop/native/bridge.c。

版本字段最多占满16字节,不保证有字符串结束符。打印前复制到17字节数组,额外补\0。版本名称只是应用元数据,Janpatch 不会自动比较设备版本。

第2步:检查长度与输出范围——不要先写了再发现放不下

**做什么:**核对输入和输出边界。

**为什么:**包头声明的大小不能直接当成可信的存储范围。

至少检查:

  • 完整包实际长度不小于56。
  • payload_size == 完整包实际长度 - 56。
  • 各必要长度非零,且能被当前接口类型表示。
  • 新程序大小不超过输出容量。
  • 三个区域互不重叠,地址计算不溢出。

**通过标志:**所有字段在有效范围内,输入完整、输出有足够空间。

第3步:核对旧程序——确保拿对了“原书”

**做什么:**核对旧程序长度,再计算旧程序 CRC32。

**为什么:**补丁是针对某份旧镜像生成的。选错旧文件,即使版本名称相近,也不能继续还原。

当前项目使用的 CRC 调用规则为:

uint32_tcrc=0xFFFFFFFFU;crc=uzlib_crc32(old_data,chunk_length,crc);/* 如果分块读取,在块之间继续沿用crc */crc=~crc;/* 所有块处理完,最后仅取反一次 */

上面old_data和chunk_length表示本次处理的数据块。整块位于 RAM 时也可以一次处理,前提是长度参数可表示。

**通过标志:**实际长度与头部一致,实际 CRC 与偏移48处的旧 CRC 一致。

CRC用于检查内容一致性和意外损坏,不等于固件签名;本文只讲还原与完整性验证。

第4步:配置并初始化 Uzlib——接好压缩输入

**做什么:**提供压缩输入缓存、长度和读取函数。

uint8_tinput_buffer[512];Unzip_Steam_T zip={0};zip.databuf=input_buffer;zip.buflen=sizeof(input_buffer);zip.size=payload_size;/* 不含56字节头部 */zip.read=read_zip;intstatus=unzip_init(&zip);if(status!=TINF_OK){/* 停止:压缩头或初始化不通过 */}

**为什么:**Uzlib 要知道“字节从哪里来、最多读多少、暂存在哪里”。

通过标志:unzip_init()返回TINF_OK。这只表明初始化/头解析通过,不代表整个压缩数据已经验证完成。

zip和input_buffer必须一直有效到结束校验完成。不能在一个临时函数内初始化局部变量后返回,再在其他地方继续使用它们。

第5步:配置 Janpatch 三个流——旧、补丁、新各归其位

Janpatch_Ctrl_T patch={0};patch.source.size=old_size;patch.source.read=read_old;patch.patch.size=patch_size;/* 解压后原始补丁长度 */patch.patch.read=read_patch;patch.target.size=new_size;patch.target.write=write_new;patch.janpatch_progress_callback=NULL;

**为什么:**算法只认识三个逻辑流,需要应用把它们连接到正确输入输出。

**通过标志:**回调有效,各大小正确。当前janpatch_cli()会把三个流的位置重置为0。

注意头文件实际叫janpatch_cil.h,虽然函数和源文件使用janpatch_cli,不要凭印象写成另一个文件名。

第6步:启动还原——让库完成数据搬运与修改

intgenerated=janpatch_cli(&patch);

**为什么:**这一步启动真正的补丁解析和新程序输出。

当前适配层已经准备工作缓存,并处理解压补丁流所需的有限回读缓存。初次移植时保留janpatch_cli.c,不要直接绕过它调用底层核心。

函数成功返回失败返回
janpatch_cli()生成的新程序字节数-1
底层janpatch()0非0

不要把这两个函数的返回值语义混淆。

第7步:完成解压校验——拿到了补丁,还要确认压缩流正常结束

intfinish=decompress_finish_chksum();if(finish!=TINF_DONE){/* 失败:不能认可输出 */}

**为什么:**恰好取得指定补丁字节,不代表压缩流结束标记和尾部校验已通过。

当前适配层要求一个完整 gzip 成员,并拒绝额外解压数据、截断、尾随数据及尾部校验错误。

**通过标志:**返回TINF_DONE,不是TINF_OK。前者表示完成,后者在初始化场景中表示可继续。

第8步:检查新程序长度——数量要对

if(generated<0||(uint32_t)generated!=new_size){/* 失败:还原或输出长度不符合预期 */}

**为什么:**输出少了或多了都不能视为目标镜像。

**通过标志:**返回值非负,且等于包头声明的新程序大小。

第9步:核对新程序 CRC——内容也要对

**做什么:**读取实际生成的新程序,按第3步相同规则计算 CRC,与头部偏移52处的值比较。

**为什么:**相同字节数也可能包含不同内容。长度匹配只能证明数量,不能证明内容。

**通过标志:**新程序 CRC 匹配。实验时,如果手头还有原始新 BIN,最好再逐字节比较一次。

第10步:宣布成功——成功是整条链路的结论

只有此前检查全部通过,才把输出标记为有效。

最终检查项对应目的
旧程序长度及CRC基础镜像正确
解压结束校验压缩输入完整且正常结束
新程序长度还原输出数量正确
新程序CRC还原内容匹配目标

**不要把“write_new被调用了”“输出区有数据了”当成功。**失败前可能已经写入了一部分结果,这些数据应保持无效状态。

六、把四个回调写成最容易理解的内存版本

以下示例要求输入输出已经是有效内存指针,且上层完成长度、类型和区域不重叠检查。它不适用于直接访问任意 MCU 的 Flash 地址,仅用于理解当前电脑验证封装。

staticconstuint8_t*old_data;staticconstuint8_t*zip_data;staticuint8_t*new_data;staticuint32_told_length,zip_length,new_length;/* 绑定时:zip_data = package + 56;只在这一层跳过包头。 */staticuint16_tread_zip(void*dst,uint16_tn,uint32_tpos){if(pos>zip_length||n>zip_length-pos)return0;memcpy(dst,zip_data+pos,n);returnn;}staticsize_tread_old(void*dst,size_tn,longpos){if(pos<0||(uint32_t)pos>old_length||n>old_length-(uint32_t)pos)return0;memcpy(dst,old_data+pos,n);returnn;}staticsize_tread_patch(void*dst,size_tn,longpos){(void)pos;/* 由当前janpatch_cli适配层管理有限回读 */if(n>UINT16_MAX)return0;returndecompress(dst,(uint16_t)n);}staticsize_twrite_new(void*src,size_tn,longpos){if(pos<0||(uint32_t)pos>new_length||n>new_length-(uint32_t)pos)return0;memcpy(new_data+pos,src,n);returnn;}

回调有三个容易犯错的点:

  1. **返回字节数。**底层驱动如果“返回0表示成功”,必须在回调里转换为成功返回n,否则 Janpatch 会认为读取/写入失败。
  2. 旧程序必须按位置读取。read_old不能忽略pos,始终从上次位置往后读。
  3. 补丁流不是压缩流。read_patch的pos属于原始补丁;不能拿它去计算压缩存储地址。当前流适配缓存与顺序解压配套,不能随意替换掉缓存逻辑。

Janpatch适配层要求每次有效读取/写入请求完整完成;底层驱动分多次操作时,应在回调内部完成再返回。Uzlib压缩输入回调可以提供不超过请求的有效字节,但为了初次移植易于检查,建议在边界内也读满请求。

七、移植时到底复制哪些文件?改哪些接口?

当前项目实际参与 DLL 构建的算法及适配源文件为:

Janpatch/Src/janpatch.c Janpatch/Src/janpatch_cli.c Uzlib/Src/tgunzip.c Uzlib/Src/tinflate.c Uzlib/Src/tinfgzip.c Uzlib/Src/crc32.c Uzlib/Src/adler32.c

加入对应Janpatch/Inc和Uzlib/Inc头文件路径,并保留所需头文件。

层次初次移植时怎么做目的
Janpatch、Uzlib算法保留当前优化版本不在接硬件前引入算法变动
janpatch_cli.c、tgunzip.c保留现有流缓存和错误处理维持顺序解压与补丁读取的配合
main.h依赖提供标准类型和必要声明脱离上一个芯片工程的头文件依赖
应用层参考bridge.c组织头部、校验和调用将包协议连接到算法
读写回调先用内存,后接存储器驱动把硬件差异限制在接口处

bridge.c中的 Windows DLL 导出宏不属于嵌入式算法要求。设备直接编译 C 源文件,不需要 ctypes,也不需要 Windows DLL。

当前优化后的适配层使用静态全局状态,属于单实例实现。一次还原必须完成后才能开始下一次;不要在多个任务或中断里同时调用。

本项目先前仅更新上位机C封装的56字节头部解析,没有同步改原嵌入式通信协议实例。接入新包时,应以本文及当前bridge.c为依据,自行衔接应用层头部处理。

八、需要多少 RAM?8位单片机能不能用?

当前主要缓存为:

缓存字节数
Uzlib 历史字典,4 KiB4096
Janpatch 旧程序/补丁/输出工作缓存,3×5121536
原始补丁当前页与上一页缓存,2×5121024
应用提供的压缩输入缓存512
小计7168,即7 KiB

另外还要计算结构体、调用栈、应用数据。如果用整块 RAM 做验证,旧程序、完整包及新程序所占 RAM 也需另算。实际设备可以通过回调把三份数据放在其他存储中,工作缓存则仍然存在。

不能只看 MCU 是8位还是32位。还需要检查:

  • RAM是否足够。
  • int、size_t、long能否表示镜像长度及偏移。
  • 编译器对标准类型、运算和存储区指针的支持。
  • 程序存储器、外部存储器如何通过驱动访问。

当前janpatch_cli()返回int,还要求旧程序、原始补丁、新程序大小以及旧程序与补丁大小之和不超过相关INT_MAX检查。若目标编译器的int为16位,常见上限32767会成为限制;size_t过窄也会限制流长度。

CRC接口的单次长度类型为unsigned int,小位宽平台需分块计算。

因此,当前实现不是“所有 MCU 只换回调即可原样运行”的承诺。先理解接口,再按目标平台评估类型和资源,才能判断是否需要进一步改造。

九、没有开发板,也能做的四组小实验

可以先使用已有上位机的双向生成和C库验证功能,观察结果;有C调试环境时,再沿bridge.c下断点。

实验怎么做预期现象想理解什么
正常还原旧BIN + 对应压缩升级包旧CRC、解压、长度、新CRC都通过完整成功链路
用错旧程序保持长度相同,替换一字节的旧BIN再验证旧CRC不匹配长度相同不代表镜像相同
包被截断复制包,删除末尾一个字节包长度检查失败越早检查,越早阻止无效输入
反向恢复使用“新到旧”包,输入原新BIN输出与原旧BIN一致回退包同样遵循源到目标的规则

“删除字节后失败”首先验证的是包总长度,不等于专门测到解压器。若要测试解压完整性,应使用保持包总长度不变的损坏样本,并记录实际失败阶段;不要把不同层的失败混为一谈。

后续调试C接口时,还可以观察:

read_zip: pos、请求长度、返回长度 read_old: pos、请求长度、返回长度 read_patch: 请求长度、实际解压长度 write_new: pos、写入长度、返回长度

先用很小的样本,日志会更容易读。调试日志仅记录偏移和长度就能看清调用关系,不必打印整份固件内容。

十、初学者常见问题

为什么只把完整包地址交给 Janpatch 不行?

完整包包含应用头和压缩数据,而 Janpatch需要的是解压后的原始补丁,必须经过应用解析和Uzlib。

unzip_init()成功,为什么还要结束校验?

初始化只是准备状态并解析压缩头,完整输入是否结束正常,要等数据消费及尾部检查。

为什么输出空间要单独准备?

还原时仍可能读取旧程序和差分包。当前教程使用独立区域,避免写入破坏输入。

上位机验证成功,能否证明任意开发板都能直接使用?

它证明了当前样本在这套实际C算法和电脑内存适配下可以还原。目标编译器、资源、地址类型和设备读写驱动仍需验证。

以后接Flash,主要变化在哪里?

旧程序读取、压缩包读取、新程序写入及CRC读取要连接真实存储器驱动;再处理擦除、写入粒度、缓存和错误上报。本文先不展开这些实现。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 10:32:24

Linux与Shell总结

一、Linux 与 Shell 基础AI 工程岗位常用 Linux 运行程序、查看日志和排查服务问题。先掌握这些命令&#xff1a;1. 文件与目录pwd # 显示当前目录 ls -la # 列出文件&#xff0c;包括隐藏文件 cd /path/to/project # 切换目录 mkdir data …

作者头像 李华
网站建设 2026/10/27 10:32:23

【落雨】三星Note8.0 N5100刷机指南GT-N5100刷LineageOS 16.0

GT-N5100 刷 LineageOS 16.0 操作清单 生成时间&#xff1a;2026-10-25 设备&#xff1a;Samsung Galaxy Note 8.0 (GT-N5100 / codename n5100 / 国行 ZC 固件 / Exynos 4412 / 2GB RAM) 需要先备份并且刷入TWRP TWRP 官方下载链接页面&#xff1a;https://dl.twrp.me/n5100/ …

作者头像 李华
网站建设 2026/10/27 10:32:17

当AI越聪明越难懂:理解悖论与效率突围

当AI越聪明越难懂&#xff1a;理解悖论与效率突围上周&#xff0c;你在读一篇AI生成的报告时&#xff0c;是否感到一种奇怪的"自以为是"? 你逐字逐句读过了&#xff0c;每个词都认识&#xff0c;连在一起却像在说外语。 不是你的理解力退步了——最新科学证据表明&a…

作者头像 李华
网站建设 2026/10/27 10:31:04

人到中年有一种处境叫做男人

人到中年&#xff0c;有一种处境叫做男人 —— 写在 1024 之后昨天 1024&#xff0c;一整天没有人对我说一句"节日快乐"。今天早上照旧爬起来加班&#xff0c;车上收音机放的是邓紫棋的《句号》。那一刻&#xff0c;一种说不出来、咽不下去的伤悲&#xff0c;从心底跑…

作者头像 李华
网站建设 2026/10/27 10:29:13

统计封闭岛屿的数目【力扣LeetCode 每日一题】

1254. 统计封闭岛屿的数目 网页链接 力扣LeetCode 力扣LeetCode & 每日一题&#xff0c;完成每日打卡&#xff0c;即可获得力扣积分。获得相应数量的积分&#xff0c;能在【力扣商店】&#xff0c;换取相应奖品&#xff01;助力每日有题做&#xff0c;丰盈积分日益多&am…

作者头像 李华