QuickBMS 内置 heatshrink 库深度解析:面向嵌入式/实时系统的低内存 LZSS 压缩方案
逆向工程CLI【免费下载链接】QuickBMSQuickBMS by aluigi - Github Mirror项目地址https://gitcode.com/gh_mirrors/qui/QuickBMS点击查看免费下载导读heatshrink 是一个面向嵌入式与硬实时系统的数据压缩/解压库它以极低的运行时内存占用最低可至 50 字节左右、可增量的有界 CPU 消耗为设计核心。本文以 src/libs/heatshrink/README.md 为骨架结合仓库中 heatshrink_encoder.c、heatshrink_decoder.c、heatshrink.c 等源码完整讲解其特性、sink/poll/finish 流式 API、窗口/前瞻/输入缓冲区三项核心参数、静态与动态内存分配两种模式以及它在 QuickBMS 解包器中的真实集成方式帮助你在 MCU 固件、流式传输或游戏资源解包场景中直接落地使用。一、heatshrink 是什么为小内存与硬实时而生heatshrink 是嵌入在 QuickBMS 仓库src/libs/heatshrink/目录下的一份独立压缩/解压库其定位非常明确——面向嵌入式系统与实时real-time系统。它基于经典的LZSS算法Lempel-Ziv-Storer-Szymanski因为 LZSS 特别适合在小内存环境下完成压缩。从 heatshrink_common.h 可以看到版本信息当前库版本为0.4.0作者为 Scott Vokes。该库的四个核心特性如下低内存占用最低约 50 字节在一些少于 50 字节内存的极端场景下依然可用对于多数一般场景内存占用小于 300 字节即可正常工作。增量式、有界 CPU 消耗输入数据可以被切分成任意小的片段逐块投喂每一次处理的工作量可控这对硬实时环境非常友好——不存在一压缩就要憋很久的不确定延迟。静态或动态内存分配皆可库本身不限制内存管理方式。动态分配malloc/free是默认行为但在嵌入式场景中你几乎总是希望改为静态分配只需在配置头文件中把宏置 0。ISC 许可证可以自由使用甚至用于商业用途见 LICENSE。需要说明的是这份 README 中的特性描述属于上游项目atomicobject/heatshrink的原始表述。在 QuickBMS 这个仓库里它扮演的是内置压缩算法库的角色与 zlib、lzma、snappy 等一同被编译进 QuickBMS供其comtype压缩/解压命令调用。二、三种集成形态命令行程序、库、QuickBMS 内嵌heatshrink 提供多种使用形态独立命令行程序heatshrink由 heatshrink.c 构建支持文件或标准输入/输出流。编码器/解码器独立库编码器和解码器可以分开独立使用。要按库的方式集成只需拷贝heatshrink_common.h、heatshrink_config.h以及heatshrink_encoder.c或heatshrink_decoder.c连同各自对应的头文件到你的工程中。同时使用两者的工程构建脚本会分别产出基于静态分配与动态分配的静态库见 Makefile 中的libheatshrink_static.a与libheatshrink_dynamic.a。QuickBMS 内嵌集成QuickBMS 将 heatshrink 编译进自身。在 src/Makefile 的第 61 行附近的源文件列表中包含了 heatshrink 相关文件src/comtype.h 第 521 行注册了QUICKBMS_COMP(HEATSHRINK)解压指令、第 957 行注册了QUICKBMS_COMP(HEATSHRINK_COMPRESS)压缩指令src/perform.c 第 2537、3255 行通过QUICK_COMP_CASE分发调用heatshrink_decompress与heatshrink_compress。下面以动态分配模式为例给出最精简的库集成文件清单heatshrink_common.h # 版本号、参数边界、字面量/回引标记 heatshrink_config.h # 动态/静态分配开关与宏 heatshrink_encoder.c/.h # 编码器压缩 heatshrink_decoder.c/.h # 解码器解压三、流式 API 基础sink、poll、finish 三步循环heatshrink 的核心编程模型是一个状态机 增量流式处理的过程。README 给出的基本用法如下使用alloc函数分配一个heatshrink_encoder或heatshrink_decoder状态机或者静态分配一个实例后调用其reset函数完成初始化。使用sink将一段输入缓冲区灌入状态机。input_size指针参数会被告知实际消费了多少字节如果为 0说明内部缓冲区已满需要先 poll 腾出空间。使用poll把输出从状态机搬运到输出缓冲区。output_size指针参数会被告知产出了多少字节返回值则指示是否还有更多输出可拿注意在收到足够输入之前状态机可能不会产生任何输出。重复步骤 2 和 3即可让数据流式通过状态机。因为做的是数据压缩输入与输出体积可能相差很大必须通过循环来处理输入、输出缓冲区的交替填满与排空。当输入流到达末尾时调用finish通知状态机没有更多输入了finish的返回值会指示是否仍有输出残留若有则继续poll。如此反复调用finishpoll直到finish表示输出已耗尽。注意finish之后若想再sink新数据必须先在状态机上调用reset。对应到源码编码器一侧的 API 原型定义在 heatshrink_encoder.hheatshrink_encoder *heatshrink_encoder_alloc(uint8_t window_sz2, uint8_t lookahead_sz2)HSE_sink_res heatshrink_encoder_sink(heatshrink_encoder *hse, uint8_t *in_buf, size_t size, size_t *input_size)HSE_poll_res heatshrink_encoder_poll(heatshrink_encoder *hse, uint8_t *out_buf, size_t out_buf_size, size_t *output_size)HSE_finish_res heatshrink_encoder_finish(heatshrink_encoder *hse)void heatshrink_encoder_reset(heatshrink_encoder *hse)解码器一侧的 API 定义在 heatshrink_decoder.h注意解码器的 alloc 多一个参数输入缓冲区大小heatshrink_decoder *heatshrink_decoder_alloc(uint16_t input_buffer_size, uint8_t expansion_buffer_sz2, uint8_t lookahead_sz2)HSD_sink_res heatshrink_decoder_sink(...)、HSD_poll_res heatshrink_decoder_poll(...)、HSD_finish_res heatshrink_decoder_finish(...)从返回值枚举可以看出这个流式模型的行为语义均定义在对应头文件中编码器返回值含义HSER_SINK_OK/HSER_SINK_ERROR_NULL/HSER_SINK_ERROR_MISUSE数据已灌入 / 参数为 NULL / API 误用如 finish 后再次 sinkHSER_POLL_EMPTY/HSER_POLL_MORE/HSER_POLL_ERROR_NULL/HSER_POLL_ERROR_MISUSE输入耗尽 / 需要再次 poll 获取更多输出 / 参数为 NULL / API 误用HSER_FINISH_DONE/HSER_FINISH_MORE/HSER_FINISH_ERROR_NULL编码完成 / 仍有输出剩余需 poll / 参数为 NULL解码器返回值含义HSDR_SINK_OK/HSDR_SINK_FULL/HSDR_SINK_ERROR_NULL已灌入可 poll / 内部缓冲区满 / 参数为 NULLHSDR_POLL_EMPTY/HSDR_POLL_MORE/HSDR_POLL_ERROR_NULL/HSDR_POLL_ERROR_UNKNOWN输入耗尽 / 还有数据需用新输出缓冲再次调用 / 参数为 NULL / 未知错误HSDR_FINISH_DONE/HSDR_FINISH_MORE/HSDR_FINISH_ERROR_NULL输出完成 / 还有输出残留 / 参数为 NULL3.1 编码器内部状态机一次 poll 内的完整流转heatshrink_encoder.c 中定义了 10 个编码器状态HSE_state枚举NOT_FULL、FILLED、SEARCH、YIELD_TAG_BIT、YIELD_LITERAL、YIELD_BR_INDEX、YIELD_BR_LENGTH、SAVE_BACKLOG、FLUSH_BITS、DONE。仓库里的 enc_sm.dotgraphviz 状态图完整刻画了这一流转sink()未填满 → 停留在not_full填满 → 进入filledfinish()也会把not_full提升为filledfilled→ 若启用索引则先构建索引 → 进入search最长匹配搜索search未找到匹配 → 产出 1 位字面量标记位找到匹配 → 产出回引标记位输入耗尽且未 finish →save_backlog把已处理数据搬回缓冲头部等待更多输入输入耗尽且正在 finish →flush_bits冲刷剩余位yield_tag_bit之后进入yield_literal逐字面量输出或yield_br_index/yield_br_length回引索引与长度分两次输出flush_bits冲刷完最后一个不完整字节后到达done。其中find_longest_matchheatshrink_encoder.c 第 452 行起负责在滑动窗口内寻找最长匹配而st_flush_bit_buffer保证末字节即使不满 8 位也会被冲刷输出这正是 README 中finish 之后 poll 直到耗尽的底层来源。3.2 解码器内部状态机与编码器严格镜像解码器状态定义在 heatshrink_decoder.c共 7 个状态HSD_state枚举TAG_BIT、YIELD_LITERAL、BACKREF_INDEX_MSB、BACKREF_INDEX_LSB、BACKREF_COUNT_MSB、BACKREF_COUNT_LSB、YIELD_BACKREF同样有 dec_sm.dot 状态图可查tag_bit弹出 1 位标记1 → 字面量0 → 回引索引位宽 ≤ 8 时直接进backref_index_lsb否则先进 MSB索引位与长度位都按高字节在前、低字节在后分两段读取对应BACKREF_INDEX_BITS window_sz2、BACKREF_COUNT_BITS lookahead_sz2见 heatshrink_decoder.c 第 121-122 行yield_backref逐字节从滑动窗口的head_index - output_index位置复制并同时写回窗口从而天然支持回引覆盖自身例如重复字节串的自身重叠展开。特别值得注意heatshrink_decoder_finishheatshrink_decoder.c 第 337 行起对处于回引索引/长度读取中间状态的情况会判断输入已空即视为FINISH_DONE——这正是 README 隐含的末字节 0 位填充可能被误读为回引场景的显式处理注释还提到输出流若以 0xFF 填充如存放于 flash也会依据输入大小直接判 done避免无意义的反复 poll。四、三个核心配置参数与 QuickBMS 的取值策略README 明确给出三个影响资源占用与压缩效果的关键配置动态分配时在alloc参数中指定静态分配时则在 heatshrink_config.h 中设定。4.1window_sz2CLI 参数-w滑动窗口大小2^W 字节含义压缩时能往回搜索多远的历史数据以寻找重复模式。window_sz2 8时窗口为 256 字节2^8window_sz2 10时为 1024 字节2^10。后者更耗内存但可能因发现更多重复而压得更小。取值范围必须在4 ~ 15之间。这一边界在 heatshrink_common.h 中由HEATSHRINK_MIN_WINDOW_BITS 4、HEATSHRINK_MAX_WINDOW_BITS 15定义并在编码器/解码器的alloc函数开头做了校验越界直接返回 NULL。编码器内存影响由于缓冲区需要同时容纳当前输入块与用于回引扫描的前一块数据编码器内部缓冲区大小为2 window_sz2见 heatshrink_encoder.c 第 79-83 行注释与代码即两倍窗口。编码格式影响每条回引的索引位宽固定等于window_sz2位因此窗口越大每条回引携带的索引位数越多。4.2lookahead_sz2CLI 参数-l前瞻/最长匹配长度上限2^L 字节含义决定单次能表示的最大重复长度。若lookahead_sz2 4一段 50 字节的连续 a 会被拆成若干条 16 字节2^4的重复模式更大的 lookahead 则可能一次表示完整。由于 lookahead 的位宽是固定的过大反而会拖累小模式——每条小回引都要白白携带多余的长度位。取值范围必须在3 ~ (window_sz2 - 1)之间。最小 3 由HEATSHRINK_MIN_LOOKAHEAD_BITS定义lookahead_sz2 window_sz2会被alloc直接拒绝这也是 test_heatshrink_dynamic.c 第 29-37 行encoder_alloc_should_reject_invalid_arguments测试所验证的行为。4.3input_buffer_size解码器输入缓冲区大小含义仅对解码器生效编码器没有这个参数决定解码器单步能处理多少输入。缓冲区越大单步工作量越大、内存占用越多极小缓冲区如 1 字节会因大量挂起/恢复suspend/resume调用而增加开销但不会改变压缩效果本身。默认值动态分配时由调用方传入QuickBMS 传的是待解压输入长度见下文静态分配时由HEATSHRINK_STATIC_INPUT_BUFFER_SIZE 32决定heatshrink_config.h 第 15 行。测试佐证test_heatshrink_dynamic.c 的集成测试以decoder_input_buffer_size 5、64等小值运行完整压缩→解压→一致性校验验证了极小输入缓冲区下的正确性。4.4 QuickBMS 集成时的实际参数取值QuickBMS 在 src/unz.c 第 6700-6733 行提供了heatshrink_decompress/heatshrink_compress两个封装函数。其参数策略非常值得借鉴解压先尝试heatshrink_decoder_alloc(insz, HEATSHRINK_MAX_WINDOW_BITS, HEATSHRINK_MAX_WINDOW_BITS)——即输入缓冲区大小直接取待解压数据长度窗口与前瞻都取最大 15失败则回退到(insz, 11, 4)窗口 2048 字节、前瞻 16 字节。压缩先尝试heatshrink_encoder_alloc(HEATSHRINK_MAX_WINDOW_BITS, HEATSHRINK_MAX_WINDOW_BITS)失败回退到(11, 4)。调用模式sink一次灌入全部输入 →finish→poll一次取出全部输出最后free。unz.c 第 6697 行还有一条重要工程经验注释输出缓冲必须比实际输出大 1 字节outsz 1否则heatshrink_decoder_poll会报错并且推荐用 0而非! 0来判断错误。在 QuickBMS 的脚本侧你可以在.bms脚本中用comtype HEATSHRINK或comtype HEATSHRINK_COMPRESS选择该算法对应 comtype.h 第 521、957 行的注册用于处理采用 heatshrink 压缩的游戏资源数据。五、静态分配 vs 动态分配嵌入式场景的关键抉择README 明确指出动态分配是默认行为但在嵌入式环境中你很可能需要静态分配——只需把 heatshrink_config.h 中的HEATSHRINK_DYNAMIC_ALLOC置为 0。该头文件完整展示了两种模式的差异#ifndef HEATSHRINK_DYNAMIC_ALLOC #define HEATSHRINK_DYNAMIC_ALLOC 1 /* 默认动态分配 */ #endif #ifdef HEATSHRINK_DYNAMIC_ALLOC #define HEATSHRINK_MALLOC(SZ) malloc(SZ) /* 可替换为自定义分配器 */ #define HEATSHRINK_FREE(P, SZ) free(P) #else #define HEATSHRINK_STATIC_INPUT_BUFFER_SIZE 32 /* 静态配置必须的三个参数 */ #define HEATSHRINK_STATIC_WINDOW_BITS 8 #define HEATSHRINK_STATIC_LOOKAHEAD_BITS 4 #endif /* 调试日志开关 */ #define HEATSHRINK_DEBUGGING_LOGS 0 /* 使用索引加速压缩需要额外空间 */ #define HEATSHRINK_USE_INDEX 1要点解释动态模式下HEATSHRINK_MALLOC/HEATSHRINK_FREE是可替换的——你可以无缝换成自己的内存池分配器这是库不限制内存管理方式的落地体现。静态模式下必须提供HEATSHRINK_STATIC_INPUT_BUFFER_SIZE、HEATSHRINK_STATIC_WINDOW_BITS、HEATSHRINK_STATIC_LOOKAHEAD_BITS三个参数默认 32 / 8 / 4即窗口 256 字节、前瞻 16 字节、输入缓冲 32 字节。此时编码器结构体直接内嵌buffer[2 window_bits]与索引数组heatshrink_encoder.h 第 72-76 行解码器结构体内嵌buffers[(1 window_bits) input_buffer_size]heatshrink_decoder.h 第 62-66 行完全不依赖堆。静态模式没有alloc/free使用方式变为直接定义一个heatshrink_encoder hse;然后调用heatshrink_encoder_reset(hse)完成初始化。Makefile 中两种库的构建也完全分离CFLAGS_STATIC ${CFLAGS} -DHEATSHRINK_DYNAMIC_ALLOC0CFLAGS_DYNAMIC ${CFLAGS} -DHEATSHRINK_DYNAMIC_ALLOC1最终产出libheatshrink_static.a与libheatshrink_dynamic.a两个静态库。六、命令行动手实践heatshrink CLI 的完整用法在src/libs/heatshrink/目录下执行make即可构建命令行工具构建依赖C99 编译器、make。完整的命令行语法定义在 heatshrink.c 的usage()第 50-84 行heatshrink [-h] [-e|-d] [-v] [-w SIZE] [-l BITS] [IN_FILE] [OUT_FILE]参数含义备注-h打印帮助-e编码压缩默认模式-d解码解压-v详细模式打印输入/输出大小、压缩率等压缩率 100% - 输出/输入 × 100%-w SIZELZSS 滑动窗口大小的以 2 为底对数推荐嵌入式-w 8其余-w 10-l BITS回引长度所用位数推荐默认-l 4所有回引固定占-w -l位过大反而低效IN_FILE/OUT_FILE输入/输出文件缺省为-标准输入/输出输入输出同名时拒绝执行防止覆盖自身命令行程序的默认值heatshrink.c 第 13-16 行DEF_WINDOW_SZ2 11窗口 2048 字节、DEF_LOOKAHEAD_SZ2 4前瞻 16 字节、DEF_DECODER_INPUT_BUFFER_SIZE 256、DEF_BUFFER_SIZE 64KB。另外proc_args第 397-441 行还隐藏了一个-i参数可调节解码器输入缓冲区大小。注意命令行工具使用getopt解析参数-w与-l的取值仍需遵循第四节所述的边界4~15 与 3~window-1超界时alloc会失败并报 failed to init encoder: bad settings。6.1 典型用法示例# 压缩文件默认 -e指定窗口 8、前瞻 4适合嵌入式数据 heatshrink -e -w 8 -l 4 input.bin output.hsz # 查看压缩率等统计信息 heatshrink -e -v -w 10 -l 4 input.bin output.hsz # 解压回原始文件 heatshrink -d -w 8 -l 4 output.hsz restored.bin # 通过标准输入/输出流式处理管道 cat input.bin | heatshrink -e -w 10 -l 4 output.hsz6.2 用 CLI 与 benchmark 脚本调参README 明确建议命令行程序可以用来测量测试数据在不同参数下的实际压缩表现这正是调参的标准方法。仓库中的 benchmark 脚本展示了完整的自动化调参流程——它基于 Canterbury Corpus 标准测试集alice29.txt、asyoulik.txt、cp.html、fields.c等 11 个文件对-w 6~12与-l 5~8的每种合法组合执行压缩→解压→文件大小比对确保无损并统计总压缩率。脚本先通过make bench触发Makefile 中的bench/corpus目标会下载解包语料DL变量可在 curl 与 wget 间切换。这套方法可同样用于你自有数据的参数选型用代表性子集穷举参数组合选压缩率与内存/耗时的折中值。七、内存占用细节与可选加速索引README 在更多信息一节补充了几个值得记下的内存数据不启用索引时整个压缩过程可在 100 字节内存以内运行。若启用可选索引HEATSHRINK_USE_INDEX 1默认开启压缩速度显著提升代价是额外占用2^(window size 1) 字节即两倍窗口 × 每个条目 2 字节代码中为buf_sz * sizeof(uint16_t)见 heatshrink_encoder.c 第 92 行且在索引构建期间会在栈上临时分配 512 字节int16_t last[256]见第 421 行。解码器无索引内存 (1 window_sz2) input_buffer_size字节heatshrink_decoder.c 第 59 行。索引的底层原理do_indexingheatshrink_encoder.c 第 402-440 行为缓冲区中每个字节位置维护一个上一个同值字节位置的扁平链表数组匹配搜索时沿链表快速跳跃显著减少逐位置线性扫描。README 在代码注释中还列出了两条未来优化方向利用 15 位空闲位动态改进索引、在不可用的前瞻区域暂存临时数据此处不再展开。八、测试体系正确性的保障仓库内测试用例分三套见 Makefile 的test_runners目标静态分配测试test_heatshrink_static.c编译期强制HEATSHRINK_DYNAMIC_ALLOC为 0第 8-10 行直接#error兜底对全局静态实例做伪随机字母数据的压缩/解压/逐字节一致性校验。动态分配测试test_heatshrink_dynamic.c涵盖encoding、decoding、regression、integration四个套件包括参数非法值拒绝第 29-37 行、NULL 参数处理、以小输入缓冲区如 5、64 字节跑完整往返等若系统装有 libtheft 并定义HEATSHRINK_HAS_THEFT还会启用基于属性的随机测试见 test_heatshrink_dynamic_theft.c。测试框架使用单头文件 greatest.h。执行方式make test # 依次运行静态与动态两套测试 make ci # 等价于 make test适合 CI 流水线九、推荐配置速查根据 README 的Recommended Defaults一节结合源码约束给出面向不同场景的参数建议场景window_sz2(-w)lookahead_sz2(-l)说明嵌入式/低内存8 ~ 10约为窗口一半如 -w 8 -l 4、-w 10 -l 5内存越紧张越靠下限一般桌面/服务端10 ~ 114 ~ 6窗口增大以提升重复检测极致高重复数据可到 15QuickBMS 的 max 尝试值不超过 window - 1每条回引索引位宽也随之增大需实测权衡通用经验lookahead_sz2从window_sz2 / 2附近起步再用代表数据实测微调任何更大窗口/更大前瞻的收益都必须用真实数据验证因为固定位宽的回引头本身就会随窗口/前瞻增大而变长break_even_point 1 window_bits lookahead_bits见 heatshrink_encoder.c 第 512-514 行——匹配长度必须超过该保本点的 1/8 才值得编码为回引。十、在 QuickBMS 中的实战位置总结heatshrink 在 QuickBMS 生态中是一个即插即用的压缩后端游戏资源如果采用 heatshrink常见于部分嵌入式平台的固件与资源打包BMS 脚本可通过comtype HEATSHRINK/comtype HEATSHRINK_COMPRESS直接调用底层由 src/unz.c 第 6700-6733 行的封装函数完成alloc(最大参数) → sink → finish → poll → free的完整流程并在 src/perform.c 第 2537、3255 行接入压缩指令分发。对于想要把 heatshrink 用到自己嵌入式项目中的读者直接复用其库文件heatshrink_encoder.c、heatshrink_decoder.c、heatshrink_common.h、heatshrink_config.h即可需注意解码端的 window/lookahead 设置必须与压缩端完全一致heatshrink_decoder.h 第 72-73 行注释明确要求window buffer 与 lookahead 设置必须匹配压缩时使用的设置。赞分享逆向工程CLI【免费下载链接】QuickBMSQuickBMS by aluigi - Github Mirror项目地址https://gitcode.com/gh_mirrors/qui/QuickBMS点击查看免费下载相关推荐Heatshrink 数据压缩库面向嵌入式及实时系统的高效解决方案Heatshrink 数据压缩库面向嵌入式及实时系统的高效解决方案 项目基础介绍与编程语言 Heatshrink 是一个专为嵌入式系统和实时环境设计的数据压缩嵌入式【亲测免费】 Heatshrink嵌入式实时系统的数据压缩库Heatshrink嵌入式实时系统的数据压缩库 项目基础介绍和主要编程语言 Heatshrink是一个专为嵌入式和实时系统设计的数据压缩和解压缩库。该项目的主嵌入式heatshrink轻量级压缩库嵌入式系统数据压缩终极解决方案heatshrink轻量级压缩库嵌入式系统数据压缩终极解决方案 在资源受限的嵌入式系统和实时环境中高效的数据处理能力是系统性能的关键。heatshrink作嵌入式上一篇Nx Generator 实战指南以 Nx 官方 Skill 文档为蓝本掌握生成器发现、选型、Dry-Run 与验证全流程下一篇TradingAgents-CN 的 LangGraph stream_mode 双模策略节点级进度跟踪与状态累积的实现剖析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考