简介:这是一份面向网络设备固件研究与调试人员的华为ONT固件编辑器图形界面工程源码,采用C++编写,适合具备一定Windows桌面开发与固件结构基础的中高级开发者,用于解析、浏览和修改ONT固件中的产品列表、子项目及高级数据格式,解决固件内容难以直观查看与编辑的问题。压缩包共41个文件、约236KB,以12个cpp与6个h源文件为核心实现,另附hpp头文件、rc资源脚本、vcxproj与sln工程配置、manifest清单,以及5张png与2张bmp截图、若干txt与md说明文档,导入解决方案即可编译研究。该资源已有1718人学习下载。代码按对话框、树形与列表事件、CRC32校验、镜像结构等模块拆分,配合ChangeLog中记录的R018、R019子项目解析、V5固件对齐、删除子项目崩溃修复等改动,便于读者理解固件分区组织与界面交互设计,并在此基础上二次开发或排错。
1. 凌晨一点,固件升级中途断电之后
手上一台 ONT(光网络终端,日常就叫光猫)升级到一半断电,再上电只剩电源灯亮着。翻出厂商给的 .bin 固件,想先核对里面的分区表,十六进制编辑器里那一串偏移量看得眼睛发酸——头部 4 个字节魔数,紧接着是头部长度、文件总长、版本号、分片数量,还有两处 CRC,靠肉眼看,错一个字节后面全乱。
这类活本来就不该用肉眼干。ONT 固件编辑器(GUI 版)就是一个 C++/Qt 写的桌面工具,把固件文件拆成文件头、分片表、子镜像三层显示,改完自动重算校验再打包回一个完整 .bin。它适合两类人:一类是需要备份、比对、局部替换固件内容的一线维护人员;另一类是想拿一个真实的二进制格式练手,搞明白 C++ 结构体解析怎么和 Qt 的 Model/View 绑起来的开发者。下面按「它是怎么搭的 → 怎么跑起来 → 怎么加一种新格式」的顺序拆。
2. 技术选型与工程骨架:C++17 + Qt 撑起一个二进制编辑器
2.1 为什么这类工具最后都落到 C++ 和 Qt 上
固件编辑器看着简单,真正做起来有三道坎:一是要按字节偏移精确读结构体,二是要处理几十上百 MB 的单文件还得界面不卡,三是要在 Windows 和 Linux 上都能编译出来给人用。这三条基本把选型框死了。
用 Python 写脚本解析固件当然快,struct.unpack几行就能出结果,但一旦要做「拖动列表、双击替换某个分片、实时刷新 CRC」这种交互,就得拖一个 GUI 框架进来,打包成单文件 exe 又是一堆麻烦。纯命令行 hexdump 那种方案更不行,改一个 4 字节的版本号要算半天偏移。
C++ 这边的优势是#pragma pack直接把二进制内存布局映射成结构体,读文件用QFile::map()走内存映射,几十 MB 的固件打开几乎无感。Qt 这边提供的是QByteArray、QDataStream这两个处理二进制很顺手的容器,以及 Model/View 架构——分片表可能有几十上百行,用QAbstractTableModel只渲染可见区域,比往QTableWidget里一行行setItem快一个数量级。
我一般会把编译器标准定在 C++17,理由是std::optional和std::string_view在处理「字段可能不存在」和「零拷贝读分片名」这两个场景下太舒服了,尤其是解析带版本差异的头部时,optional能把「这个字段这一版没有」写得比返回 -1 干净得多。
2.2 三层拆法:解析层、模型层、界面层各管什么
这个项目的代码组织是典型的三层,边界划得很清楚,二次开发时基本不用动别的层。
解析层只认字节,不认 Qt。它拿到一个const uint8_t*加长度,吐出一个FirmwareImage结构,里面装着头部字段和分片数组。这一层不 include 任何 Qt 头文件,好处是能单独拎出来写单元测试,也能被命令行工具复用。
模型层是胶水,负责把FirmwareImage转成 Qt 的QAbstractTableModel,同时管住「脏数据」状态——哪个分片被替换过、CRC 需要重算、文件是否已保存。这一层是很多人写工具时会忽略的,直接让界面去改解析结果,改到最后自己都说不清当前内存里的是原始数据还是修改后的数据。
界面层只做两件事:把用户操作翻译成模型层的方法调用,把模型层的变化渲染出来。它不应该出现任何offset + 4之类的偏移计算。
三层之间的数据流是单向的:打开文件 → 解析层产出FirmwareImage→ 模型层持有它并生成表格 → 界面读模型。保存时反过来走一遍:界面触发 → 模型层用解析层的打包函数重新序列化 → 写盘。
2.3 工程骨架与构建环境
构建这块,老项目常见做法是 qmake(.pro 文件),新一点的会迁到 CMake。两种都能用,我倾向 CMake,因为加第三方库(比如后面要提的 zlib 解压)时find_package比 .pro 里的LIBS +=好维护。
Qt 版本上,Qt 5.15 和 Qt 6.x 都能编译,但要注意 Qt 6 把QTextCodec挪到了 core5compat 模块,如果代码里有用到 GBK 转码读固件里的中文分区名,迁版本时会直接编译不过。这是我在一个老分支上真踩过的,编译报错指向一行看起来完全没问题的字符串处理。
提示:第一次编译前先确认
qmake -v或cmake --version输出的 Qt 路径,和 IDE 里选中的 Kit 是同一套。Kit 选错导致的「找不到 QApplication」能浪费半小时。
工程里还有个resources.qrc,装着图标和一份内置的固件格式描述表。这份描述表是纯文本,列了每种格式的魔数、头部长度、分片表偏移,新增格式时改这个文件比改代码快。
3. 核心模块拆解:一个固件文件从打开到列表显示
3.1 目录结构:源码怎么分布
先看仓库大致长什么样。这类工具的项目规模通常在几十个文件,目录不会太深:
fweditor/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp # 入口,只负责拉起主窗口 │ ├── core/ # 解析层,不含 Qt │ │ ├── firmware_header.h # 头部结构体定义 │ │ ├── firmware_image.cpp # 解析/打包主逻辑 │ │ ├── crc32.cpp # 校验算法 │ │ └── section.h # 分片描述 │ ├── model/ │ │ ├── section_model.h # QAbstractTableModel 子类 │ │ └── section_model.cpp │ └── ui/ │ ├── main_window.ui # Qt Designer 布局 │ ├── main_window.cpp │ └── replace_dialog.cpp # 替换分片的对话框 ├── resources/ │ ├── resources.qrc │ └── formats.json # 内置格式描述表 └── tests/ └── test_parse.cpp # 解析层单测,不依赖 Qt GUIcore/能脱离 Qt 单独编译,这一点很关键。我在改这类项目时,第一件事就是把core/抽成一个 static library,然后写个 main 函数手动喂几个固件文件进去,确认解析结果对得上,再回去动界面。不然界面一崩,你根本分不清是解析错了还是渲染错了。
3.2 文件头结构体与字节序对齐
固件头部是整个工具的入口。常见做法是用#pragma pack(push, 1)关掉编译器填充,让结构体大小严格等于字段宽度之和:
// src/core/firmware_header.h #pragma once #include <cstdint> #pragma pack(push, 1) // 关键:关掉对齐填充,否则 sizeof 会比预期大 struct FirmwareHeader { char magic[4]; // 魔数,用来识别固件类型,例如 "FWHD" uint32_t headerSize; // 头部自身长度,留作版本扩展用 uint32_t fileSize; // 整个固件的总字节数 uint32_t version; // 版本号,高位主版本、低位次版本 uint32_t sectionCount; // 分片数量 uint32_t headerCrc; // 头部(不含本字段)的 CRC32 uint32_t dataCrc; // 数据区整体的 CRC32 uint32_t reserved; // 保留字段,读的时候直接跳过 }; #pragma pack(pop) static_assert(sizeof(FirmwareHeader) == 32, "头部必须是 32 字节");那段static_assert是保命的。不同编译器、不同平台对#pragma pack的处理偶尔会有出入,加一条编译期断言,一旦哪天有人往结构体里插了个字段忘了改断言,编译直接挂掉,比运行到一半读错偏移强太多。
headerSize这个字段容易被新手忽略,觉得冗余——反正结构体大小固定。但实际固件是有版本演进的,新版本可能在头部末尾追加字段,老版本没有。解析时用headerSize而不是sizeof(FirmwareHeader)去算数据区起点,才能同时兼容两代固件。我见过一个改版就是死在这里:用sizeof算偏移,遇到新固件后所有分片位置整体错 8 字节,界面上显示的分片名全是乱码。
字节序上,这类固件绝大多数是小端(x86/ARM 常规选择)。读的时候别直接reinterpret_cast完事,虽然 x86 上侥幸能跑,但换到某些 ARM 平台就是错的。稳妥写法是走一遍qFromLittleEndian,或者自己写个readU32LE:
// 从字节流里按小端读一个 uint32,offset 是相对 data 起点的偏移 static inline uint32_t readU32LE(const uint8_t* data, size_t offset) { return static_cast<uint32_t>(data[offset]) | (static_cast<uint32_t>(data[offset + 1]) << 8) | (static_cast<uint32_t>(data[offset + 2]) << 16) | (static_cast<uint32_t>(data[offset + 3]) << 24); }这个函数看起来笨,但它是可移植的、不依赖对齐的,也不会因为data + offset没对齐而触发某些架构上的 bus error。解析热点路径上调用几万次也无所谓,现代编译器能把它优化成一条mov。
3.3 分片表解析与 QAbstractTableModel 绑定
头部后面紧跟分片表,每个表项描述一个子镜像:
#pragma pack(push, 1) struct SectionEntry { char name[16]; // 分片名,ASCII,不足补 0 uint32_t offset; // 相对文件起点的偏移 uint32_t length; // 分片字节数 uint32_t loadAddr; // 加载地址,烧录工具会用,编辑器只做展示 uint32_t crc32; // 该分片自身的 CRC32 }; #pragma pack(pop)解析循环里必须做边界检查,这是整个工具最容易翻车的地方。sectionCount是固件里写死的数字,如果文件被截断或者被人手工改过,这个值可能是 0xFFFFFFFF,直接循环就是越界读。稳妥的写法是先算一遍「分片表最多能放几个」:
bool parseSections(const uint8_t* data, size_t size, const FirmwareHeader& hdr, QVector<SectionEntry>& out) { const size_t tableStart = hdr.headerSize; // 表从头部之后开始 const size_t maxCount = (size - tableStart) / sizeof(SectionEntry); if (hdr.sectionCount > maxCount) { return false; // 声明的分片数超过文件实际能容纳的,直接判为损坏 } for (uint32_t i = 0; i < hdr.sectionCount; ++i) { SectionEntry e{}; std::memcpy(&e, data + tableStart + i * sizeof(SectionEntry), sizeof(e)); // 每个分片的数据区也必须落在文件内 if (e.offset > size || e.length > size - e.offset) { return false; } out.push_back(e); } return true; }这里两处判断都是「减法写法」而不是「加法比较」:e.length > size - e.offset而不是e.offset + e.length > size。原因是offset + length在 32 位无符号下会溢出,一个精心构造的畸形固件能让加法回绕到小值,检查直接失效。这种细节在安全评审里经常被点出来,写固件工具的人尤其该养成习惯。
模型层把QVector<SectionEntry>包成表格。行数是分片数,列是名称、偏移、长度、加载地址、CRC 状态:
QVariant SectionModel::data(const QModelIndex& idx, int role) const { if (!idx.isValid() || role != Qt::DisplayRole) return {}; const SectionEntry& s = m_sections[idx.row()]; switch (idx.column()) { case 0: return QString::fromLatin1(s.name, strnlen(s.name, 16)); case 1: return QString("0x%1").arg(s.offset, 8, 16, QChar('0')); case 2: return QString::number(s.length); case 3: return QString("0x%1").arg(s.loadAddr, 8, 16, QChar('0')); case 4: return m_dirtyRows.contains(idx.row()) ? QStringLiteral("待重算") : QStringLiteral("OK"); default: return {}; } }strnlen那个用法是有意为之:分片名是定长 16 字节的字符数组,如果固件里没补 0,fromLatin1(s.name)会一直读到数组边界外。用strnlen限定在 16 以内,读出来的就是干净的。m_dirtyRows是个QSet<int>,记录哪些行被替换过,界面上标成「待重算」提示用户保存前会重算 CRC。
3.4 CRC32 校验模块与重打包流程
校验是这个工具存在的核心理由之一。固件头里两处 CRC——头部自身一处、数据区一处——加上每个分片各一处,改任何一个字节都要重算对应的那几处,漏一个设备就不认。
常见做法是标准 CRC-32(多项式 0xEDB88320,反射算法),查表实现:
// 懒加载的表,第一次调用时构建,之后 O(1) 查表 static uint32_t g_table[256]; static bool g_tableReady = false; static void buildTable() { for (uint32_t i = 0; i < 256; ++i) { uint32_t c = i; for (int k = 0; k < 8; ++k) c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1); g_table[i] = c; } g_tableReady = true; } uint32_t crc32(const uint8_t* data, size_t len, uint32_t seed = 0) { if (!g_tableReady) buildTable(); uint32_t c = seed ^ 0xFFFFFFFFu; // 初值取反是标准 CRC-32 的约定 for (size_t i = 0; i < len; ++i) c = g_table[(c ^ data[i]) & 0xFF] ^ (c >> 8); return c ^ 0xFFFFFFFFu; // 结果再取反 }seed参数是为「分段累加」准备的:如果固件很大,不想一次性把整块内存读进来算,可以分块调用,把上一块的返回值当下一块的 seed 传进去,结果和一次性算完一致。这个性质在很多 CRC 实现里成立,但前提是算法本身不做终值处理——所以取反操作必须放在函数内外各一次,不能省。
重打包的顺序是有讲究的,错了就是白改:
- 拿原始
FirmwareImage,把被替换分片的数据写回; - 逐个分片重算分片 CRC,写回
SectionEntry.crc32; - 把分片表整体序列化回字节流;
- 重算数据区 CRC(覆盖分片表 + 所有分片数据),写回
dataCrc; - 重算头部 CRC(覆盖头部除
headerCrc外的所有字节),写回headerCrc; - 更新
fileSize——注意这一步必须在第 5 步之前,因为fileSize参与头部 CRC 计算。
第 6 步是最容易漏的。我见过有人替换了一个更长的分片,fileSize没跟着更新,写出来的固件校验全过,但烧录工具读到一半就报「文件长度不符」。这种 bug 特别阴,因为编辑器自己不报错,你只有把文件扔给烧录端才知道。
4. 跑起来:从编译到完成一次固件分片替换
4.1 准备依赖并编译
环境按 CMake + Qt 5.15 走,Windows 下用 MSVC 2019 或 MinGW 都行,Linux 下apt install qtbase5-dev就够。依赖只有 Qt Core、Qt Widgets 和 zlib(部分固件分片是压缩的,要解压后才能看内容)。
# 1. 建构建目录,保持源码树干净 mkdir -p build && cd build # 2. 配置。CMAKE_PREFIX_PATH 指向 Qt 的安装前缀, # 路径不对的话 find_package(Qt5) 会直接失败 cmake .. -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/opt/Qt/5.15.2/gcc_64 # 3. 编译,-j 后面跟 CPU 核心数,8 核机器上大概 40 秒出结果 cmake --build . -j8编译产物在build/bin/下,Windows 是一个 exe 加几个 Qt 的 dll,Linux 下是单个可执行文件。如果 exe 双击报「找不到 Qt5Core.dll」,那是windeployqt没跑,在 exe 同目录执行windeployqt fweditor.exe补齐运行时。
CMAKE_BUILD_TYPE一定要设Release。Debug 版在打开大固件时慢得离谱,因为QByteArray的拷贝在 Debug 下不走内联,几十 MB 的数据来回拷几次,界面直接卡死,你会误以为是代码有性能问题。
4.2 打开固件:看头部信息和分片清单
启动后主界面分三块:左上是一排按钮(打开、保存、另存、校验),右上是一块「头部信息」表格,下面占最大面积的是分片列表。
打开文件走的是QFile::map(),把整个文件映射进地址空间,解析层直接在映射指针上跑,不额外拷贝:
void MainWindow::openFirmware() { const QString path = QFileDialog::getOpenFileName( this, tr("选择固件"), {}, tr("固件文件 (*.bin *.img);;所有文件 (*)")); if (path.isEmpty()) return; QFile f(path); if (!f.open(QIODevice::ReadOnly)) { /* 弹错误框 */ return; } uchar* p = f.map(0, f.size()); // 内存映射,返回 nullptr 表示失败 if (!p) { /* 映射失败,可能是文件过大或权限问题 */ return; } FirmwareImage img; if (!img.parse(p, static_cast<size_t>(f.size()))) { QMessageBox::warning(this, tr("解析失败"), tr("文件头校验不通过,可能不是该格式的固件")); return; } m_model->setImage(std::move(img)); // 交给模型层,界面自动刷新 }f.map()返回的是映射区首地址,生命周期绑定在QFile对象上,所以QFile f不能是局部临时变量——必须是成员或者在parse期间保持存活。早期版本我把QFile写在if块里,出块之后映射就失效了,界面显示全对,一点「保存」就段错误。这种玄学问题查了大半天。
头部的几个字段建议在界面上直接展示成可读形式:魔数转成 ASCII、fileSize和实际文件大小并排显示(不一致就是文件被截断)、两处 CRC 显示「声明值 vs 实算值」,对不上就标红。这个对比视图能省掉大量排查时间。
4.3 替换分片并回写
替换分片的操作流程:在列表里选中一行 → 点「替换」→ 选一个新文件 → 工具读入新内容、覆盖到该分片 → 标记为脏行。
替换时有个隐含约束:新分片长度能不能变?取决于固件格式。如果分片表里存的是绝对偏移,那替换成不同长度就必须同时平移后面所有分片的offset,还得更新fileSize。如果存的是相对偏移(每个分片头部自带长度),那就不用动别人。
这个项目里两种都可能遇到,所以替换逻辑要做成可配置的:
bool FirmwareImage::replaceSection(int index, const QByteArray& newData) { if (index < 0 || index >= m_header.sectionCount) return false; SectionEntry& s = m_sections[index]; const bool sizeChanged = (newData.size() != static_cast<int>(s.length)); if (sizeChanged && !m_layout.allowResize) { return false; // 该格式不允许变长,直接拒绝 } // 用一个"逻辑分片"模型暂存数据,打包时再统一算偏移 m_payload[index] = newData; s.length = static_cast<uint32_t>(newData.size()); m_dirtyRows.insert(index); return true; }注意这里没有立刻去改s.offset。偏移的重排放到打包阶段统一做,因为连续替换多个分片时,中间态偏移没有意义,边改边算很容易算重。这是我在做一个类似工具时改出来的:早期版本每次替换都立刻重排偏移,结果连续替换三个分片后偏移全错,因为第二次替换基于的是第一次的中间态。抽成「先改内容、后重排」之后就没再出过这类问题。
4.4 结果自检:三个必须核对的地方
保存前手动点一次「校验」,会跑一遍完整重算并和声明值比对。三个地方必须看:
第一个是文件总长。保存后QFileInfo::size()要和头部里的fileSize相等。差一个字节都说明序列化逻辑有问题。
第二个是分片偏移的连续性。把分片按offset排序,检查每一段的offset + length是否等于下一段的offset。中间有空洞说明重排有 bug。这个检查我一般会写成临时断言塞在打包函数末尾,出问题立刻能定位到哪一段断了。
第三个是CRC 三方一致。界面上算一遍、重新打开保存后的文件再算一遍、如果手边有厂商的校验工具最好也跑一遍。三方一致才算真的对。只信自己算的那一遍,等于自己给自己判卷。
注意:自检用的固件一定要先备份原始文件。工具本身的 bug 加上手滑,一次就能把唯一的固件副本写坏,没有后悔药。
5. 避坑记录:固件编辑器最容易翻车的五件事
5.1 现象:打开固件直接闪退,没有任何错误提示
原因通常是结构体对齐没关。如果#pragma pack(push, 1)写在了头文件里但没配对pop,或者被后面的 include 打断,sizeof(FirmwareHeader)会变成 36 甚至 40,读headerSize的偏移就全错,sectionCount读出来是个天文数字,循环直接越界。
解决办法是加编译期断言static_assert(sizeof(FirmwareHeader) == 32),并且确保pack和pop严格配对,中间不留其他 include。另外在解析入口先做一次魔数比对,魔数不对直接返回,别急着往下走。
5.2 现象:改了内容保存后,固件大小没变但设备不认
八成是fileSize没更新,或者更新了但在头部 CRC 计算之后。因为fileSize是头部 CRC 的覆盖范围之一,顺序错了算出来的 CRC 是旧值。
解决顺序必须是:先确定所有分片的新偏移 → 算出新的总长写进fileSize→ 序列化分片表 → 算数据区 CRC → 最后算头部 CRC。写完之后把这段顺序用注释钉在打包函数开头,谁改谁看见。
5.3 现象:替换了一个更长的分片,界面上看着都对,保存出来的文件烧不进去
偏移量只平移了后面的分片,但没考虑分片表本身也可能变长——如果换了更多的分片或者分片名更长,表区会占用更多空间,所有偏移的基准都要跟着变。
解决方式是把「表区长度」当成一个变量参与布局计算,而不是写死。打包时先算tableStart = headerSize、tableEnd = headerSize + count * sizeof(SectionEntry),数据区从tableEnd往后排,每个分片依次分配偏移。这样无论分片数怎么变,布局都是自洽的。
5.4 现象:Linux 上编译通过,Windows 上分片名全是乱码
char name[16]在 MSVC 下默认是signed char,QString::fromLatin1接收的是const char*,某些构造重载下高位字节被当成负数处理。更常见的原因是源码文件编码不一致,字符串字面量在两种编译器下解释不同。
解决办法是分片名统一按QString::fromLatin1(reinterpret_cast<const char*>(s.name), len)处理,显式指定长度,不依赖'\0'终止。源码文件统一存成 UTF-8 无 BOM,CMake 里加/utf-8(MSVC)编译选项。
5.5 现象:打开 200MB 的固件卡死十几秒,之后操作也一顿一顿的
直接原因是用了QByteArray拷贝整份数据,再加上表格模型每次刷新都重算全部 CRC。data()里调crc32()是最典型的性能杀手,Qt 的视图会为每一行、每一列都调用一次data()。
解决办法分两步:数据侧用QFile::map()零拷贝,只在真正需要修改时才复制;CRC 侧缓存结果,data()里只读缓存值,重算交给显式的「校验」按钮或者延迟到保存时。缓存用一个QHash<int, uint32_t>挂在模型上就够。
6. 二次开发:给编辑器加一种新固件格式
接手这类工具最常见的需求就是「再支持一种格式」。如果解析层是硬编码的if (magic == "FWHD"),加第三种格式就开始难受了。
我的做法是先抽一个解析接口出来,让每种格式自己认得自己:
// src/core/format_plugin.h class IFormatPlugin { public: virtual ~IFormatPlugin() = default; virtual QString name() const = 0; // 显示名 virtual bool sniff(const uint8_t* d, size_t n) const = 0; // 魔数匹配 virtual std::unique_ptr<FirmwareImage> parse(const uint8_t* d, size_t n) = 0; virtual QByteArray pack(const FirmwareImage& img) = 0; // 序列化回字节流 };有了这个接口,新增格式就是加一个类、在注册表里塞一行,主窗口只跟IFormatPlugin*打交道。sniff()只读前 4 个字节,打开文件时把所有插件过一遍,第一个返回 true 的胜出。这样即使两种格式魔数前缀相同、靠后面的字段区分,也能通过更细的sniff()实现处理。
注册用一张静态表就够,不必上什么反射框架:
// 在 main.cpp 里集中注册,加格式只改这一处 static void registerPlugins(PluginRegistry& reg) { reg.add(std::make_unique<FwEditorFormat>()); // 原有格式 reg.add(std::make_unique<AltHeaderFormat>()); // 新加的 }验证新格式走三步,别跳。第一步,用同一份固件让新插件parse再pack,比较输出和输入是否逐字节相同——这是最强的正确性证明,字节级一致说明解析和序列化互为逆运算,没有丢字段。第二步,故意改一个分片再打包,把文件扔给真正的烧录端认一次。第三步,构造几个畸形输入(截断的、sectionCount造假的、CRC 全 0 的),确认解析器返回 false 而不是崩溃。
几个容易忽略的点列在下面:
| 关注点 | 容易犯的错 | 建议做法 |
|---|---|---|
| 长度字段 | 用sizeof代替头部里声明的长度 | 一律读headerSize,sizeof只用来做断言 |
| 变长分片 | 替换后只改length不改offset | 打包阶段统一重排,别在替换时改 |
| CRC 顺序 | 头部 CRC 在fileSize之前算 | 顺序写死在注释里,按注释走 |
| 边界检查 | 用offset + length > size判断 | 改成length > size - offset防溢出 |
| 平台差异 | 依赖char有符号性 | 显式reinterpret_cast+ 指定长度 |
最后一条血泪经验:新格式的解析器写完后,一定拿至少三份不同版本的固件各跑一遍,光跑一份看不出兼容性问题。我之前只测了一份老固件,上线之后遇到新版本固件头部多出 8 字节扩展字段,所有偏移整体错位,界面上分片名全是乱码,排查了半天才发现是headerSize用错了。从那以后我每次加新格式,都强制走一遍「三份固件 + 一份畸形样本」的回归,少一份都不算完。希望帮到你。
本文还有配套的精品资源,点击获取