简介:HexEdit是一款在VC++环境下开发的十六进制编辑控件,面向需要处理二进制数据的软件开发者、系统维护人员及逆向工程学习者,用于解决配置文件、日志、数据库、图像等二进制文件的查看与修改需求。资源包共53个文件,以h头文件、cpp源文件、obj编译产物为主,另含sln解决方案、vcxproj工程文件、rc资源脚本及exe可执行程序等,压缩包约2.02MB,结构完整,便于直接编译与二次开发。控件提供读写文件API、数据渲染、查找替换、插入删除等接口,并支持二进制与ASCII双视图模式,配套演示项目展示了集成与基本用法。目前已有97人学习下载,适合希望快速掌握十六进制编辑功能实现思路、复用成熟控件或扩展自定义编辑能力的开发者参考。
1. 十六进制编辑控件:为什么我最终选了 hexedit 而不是自己造轮子
去年帮一个做固件逆向的朋友处理一批二进制配置文件,需要在 VC 环境里嵌一个能直接改字节的编辑控件。一开始想的是自己用 MFC 的 CEdit 加一层 Hex 转换逻辑,结果光处理光标定位、半字节编辑、非打印字符显示这三件事就写了一周,还全是 bug。后来换成了 hexedit 十六进制编辑控件,半天就跑通了。这个控件本质上是一个可嵌入 Windows 应用的十六进制编辑组件,支持在 VC、C++ Builder、Delphi 等环境里直接调用,核心能力是打开任意二进制文件、按字节或半字节编辑、实时显示 ASCII 对照区、支持大文件分页加载。它适合三类人:需要在自研工具里集成 Hex 编辑功能的开发者、做固件或协议逆向的工程师、以及教学场景下需要可视化二进制结构的场景。如果你只是偶尔看看文件头,用 HxD 就够了;但如果你要把 Hex 编辑能力做进自己的 EXE 里,这个控件能省掉至少两周的底层开发时间。
2. hexedit 控件的核心机制与 VC 集成方式
2.1 控件的数据模型:为什么它处理大文件不卡
hexedit 内部用的是分页缓冲模型,不是一次性把整个文件读进内存。它维护一个固定大小的页缓冲(常见做法是 64KB 一页),只加载当前视口附近的字节。当你滚动到文件末尾时,控件触发按需读取,从磁盘流里拉取对应偏移的数据。这个设计直接决定了它能处理几百 MB 甚至上 GB 的二进制文件而不爆内存。
理解这个模型对用好控件很关键。比如你在做固件分析时,文件可能有 256MB,但实际你只关心偏移 0x0000 到 0x1000 的头部区域和 0x0F0000 附近的配置区。hexedit 不会因为文件大就变慢,因为它只读你看到的那部分。但这也意味着,如果你在代码里调用了“获取全部数据”的接口,那内存开销还是跑不掉——控件本身不背这个锅。
另一个核心机制是编辑缓冲区。你对字节的修改先写进内存里的脏页标记区,只有调用保存接口时才真正回写磁盘。这个设计给了你“后悔药”:改错了可以直接丢弃缓冲区重新加载。但要注意,如果你在编辑过程中程序崩溃,未保存的修改就丢了。我一般会在关键编辑操作后手动触发一次保存,或者在自己的应用层加一个定时持久化逻辑。
2.2 在 VC 里加载控件:从注册到第一个 Hex 窗口
在 VC 中使用 hexedit 控件,常见做法是把它作为 ActiveX 控件或静态库集成。如果是 ActiveX 形式,你需要先注册控件,然后在对话框资源里插入。下面是一个典型的初始化流程,假设你已经拿到了控件的开发包。
// 假设控件已经通过 ActiveX 注册,在对话框类里声明成员变量 // m_hexEdit 是与控件关联的变量 BOOL CMyHexToolDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 1. 设置控件为只读模式(初始加载时避免误编辑) m_hexEdit.SetReadOnly(TRUE); // 2. 打开目标二进制文件 CString strFilePath = _T("C:\\firmware\\config.bin"); if (!m_hexEdit.OpenFile(strFilePath)) { AfxMessageBox(_T("文件打开失败,检查路径和权限")); return FALSE; } // 3. 设置显示参数:每行16字节,显示ASCII区 m_hexEdit.SetBytesPerLine(16); m_hexEdit.SetShowAscii(TRUE); // 4. 跳转到指定偏移,比如配置区起始位置 m_hexEdit.SetCaretPos(0x00001000); // 5. 解除只读,允许编辑 m_hexEdit.SetReadOnly(FALSE); return TRUE; }这段代码的逻辑说明:第一步先设只读,是因为加载大文件时如果控件处于可编辑状态,某些版本会在渲染过程中触发意外的修改事件。第二步 OpenFile 是核心,它内部会走分页加载逻辑,不会阻塞 UI 太久。第三步 SetBytesPerLine 决定了显示密度,16 是常见值,但如果你在看网络包结构,8 字节一行更利于对照协议字段。第四步跳转偏移用的是十六进制,注意这里传的是整数,不是字符串。第五步解除只读后,用户就可以在控件里直接改字节了。
参数方面,SetBytesPerLine 支持 1 到 64 之间的值,但超过 32 之后 ASCII 区会换行显示,视觉上比较乱。SetShowAscii 默认是开的,如果你只关心 Hex 值可以关掉,能多显示一列字节。SetCaretPos 的偏移量是相对于文件开头的绝对偏移,不是当前页偏移,这个容易搞混。
2.3 编辑事件的捕获与数据回写
控件在用户修改字节时会触发通知消息,你需要在父窗口的消息映射里处理。常见做法是捕获“字节已修改”事件,然后更新你自己的数据结构或界面状态。
// 消息映射里添加 ON_NOTIFY(HEXEDIT_BYTE_CHANGED, IDC_HEXEDIT_CTRL, &CMyHexToolDlg::OnHexByteChanged) // 处理函数 void CMyHexToolDlg::OnHexByteChanged(NMHDR* pNMHDR, LRESULT* pResult) { LPNMHEXEDIT pHex = (LPNMHEXEDIT)pNMHDR; // pHex->nOffset 是修改的字节偏移 // pHex->bOldValue 是原值 // pHex->bNewValue 是新值 CString strLog; strLog.Format(_T("偏移 0x%08X: 0x%02X -> 0x%02X"), pHex->nOffset, pHex->bOldValue, pHex->bNewValue); m_listLog.AddString(strLog); // 标记文件已修改,提示用户保存 m_bModified = TRUE; SetWindowText(_T("Hex 编辑器 *")); *pResult = 0; }这里的关键参数是 nOffset,它是文件内的绝对偏移,你可以用它来定位到自己的数据结构里对应的字段。bOldValue 和 bNewValue 是单字节值,范围 0x00 到 0xFF。如果你在做协议逆向,可以在这个回调里加一层校验逻辑,比如某个字段只允许特定值,不合法就弹窗提示并回滚。
保存操作调用控件的 SaveFile 或 SaveAs 接口。注意,SaveFile 是原地覆盖,SaveAs 是另存。我一般建议用户走 SaveAs,保留原始文件作为备份,这是血泪经验——曾经有一次改固件校验和,原地保存后发现改错了,原始文件没了,只能重新从设备里 dump。
3. 用 hexedit 做二进制结构解析:从裸字节到可读字段
3.1 定义字段映射表:把偏移变成有意义的名字
光看十六进制数字没意义,你得知道偏移 0x10 处的 4 个字节是版本号还是长度字段。常见做法是在应用层维护一张字段映射表,然后在 hexedit 的选区变化事件里查表显示。
// 字段定义结构 struct FieldDef { DWORD offset; DWORD length; CString name; CString type; // "uint32", "ascii", "bytes" }; // 初始化映射表 std::vector<FieldDef> g_fields = { {0x00, 4, _T("Magic"), _T("ascii")}, {0x04, 4, _T("Version"), _T("uint32")}, {0x08, 4, _T("DataLength"), _T("uint32")}, {0x0C, 16, _T("Reserved"), _T("bytes")}, {0x1C, 32, _T("DeviceName"), _T("ascii")}, {0x3C, 4, _T("Checksum"), _T("uint32")}, }; // 在选区变化时查表 void CMyHexToolDlg::OnHexSelChanged(NMHDR* pNMHDR, LRESULT* pResult) { LPNMHEXEDIT pHex = (LPNMHEXEDIT)pNMHDR; DWORD selStart = pHex->nOffset; for (const auto& f : g_fields) { if (selStart >= f.offset && selStart < f.offset + f.length) { CString strInfo; strInfo.Format(_T("字段: %s | 类型: %s | 偏移: 0x%04X | 长度: %d"), f.name, f.type, f.offset, f.length); m_staticFieldInfo.SetWindowText(strInfo); break; } } *pResult = 0; }这段代码的价值在于:当你点击 hexedit 里某个字节时,界面下方会显示这个字节属于哪个字段、什么类型、多长。做固件分析时,这比拿计算器手算偏移快十倍。参数上,offset 和 length 必须和实际二进制布局严格一致,错一个字节后面全乱。type 字段是给你自己看的,控件本身不解析类型,解析逻辑要你自己写。
3.2 校验和计算与自动修正
二进制文件里经常有校验和字段,改完数据后校验和就失效了。常见做法是在保存前自动重算校验和并回写。
// 假设校验和是简单的累加和,从偏移0到0x3B,结果存在0x3C void CMyHexToolDlg::RecalcChecksum() { BYTE buffer[0x3C]; // 从控件读取数据,注意这里要按偏移读 for (int i = 0; i < 0x3C; i++) { buffer[i] = m_hexEdit.GetByteAt(i); } DWORD sum = 0; for (int i = 0; i < 0x3C; i++) { sum += buffer[i]; } sum = sum & 0xFFFFFFFF; // 截断为32位 // 写回校验和字段,小端序 m_hexEdit.SetByteAt(0x3C, (BYTE)(sum & 0xFF)); m_hexEdit.SetByteAt(0x3D, (BYTE)((sum >> 8) & 0xFF)); m_hexEdit.SetByteAt(0x3E, (BYTE)((sum >> 16) & 0xFF)); m_hexEdit.SetByteAt(0x3F, (BYTE)((sum >> 24) & 0xFF)); AfxMessageBox(_T("校验和已更新")); }逻辑说明:GetByteAt 和 SetByteAt 是逐字节读写接口,适合小范围操作。如果你要读大块数据,用 ReadBlock 更高效。参数上,小端序是 x86 平台的常见存储方式,低字节在前。如果你的目标平台是大端序,写入顺序要反过来。注意,这个例子用的是累加和,实际固件里可能是 CRC16、CRC32 或更复杂的哈希,算法要按实际协议来。
3.3 搜索与替换:在二进制里找特征码
hexedit 控件通常自带搜索接口,但如果你要批量替换或按模式匹配,得自己写一层。
// 搜索特征码,返回偏移列表 std::vector<DWORD> SearchPattern(const std::vector<BYTE>& pattern) { std::vector<DWORD> results; DWORD fileSize = m_hexEdit.GetFileSize(); DWORD pos = 0; while (pos < fileSize) { DWORD found = m_hexEdit.FindBytes(pattern.data(), pattern.size(), pos); if (found == (DWORD)-1) break; results.push_back(found); pos = found + 1; // 继续往后找 } return results; }FindBytes 的第三个参数是起始偏移,返回第一个匹配的位置,没找到返回 -1。这个接口内部走的是分页缓冲,所以即使文件很大,搜索也不会一次性加载全部数据。但要注意,如果你搜的模式跨页边界,某些版本的控件可能漏掉,稳妥做法是搜到页尾时手动处理边界重叠。
4. 避坑与排查:hexedit 集成中翻过的车
4.1 控件注册失败,对话框里显示空白
现象:在 VC 对话框资源里插入 hexedit 控件后,编译通过但运行时控件区域一片空白,没有任何报错。
原因:ActiveX 控件没有正确注册,或者项目没有启用 ActiveX 支持。常见情况是开发机上注册了,但部署到客户机上没注册。
解决:在目标机上用 regsvr32 注册控件对应的 ocx 文件。如果是静态库版本,检查链接器是否把库文件加进去了。另外,VC 项目属性里要确认“使用 ActiveX 控件”选项是勾上的。
4.2 打开大文件时界面卡死
现象:加载一个 500MB 的二进制文件时,程序界面无响应十几秒。
原因:控件默认的页缓冲大小可能偏小,导致频繁触发磁盘读取。或者你在打开文件后立刻调用了全量读取接口。
解决:把页缓冲调大,常见做法是设到 256KB 或 512KB。另外,不要在 OnInitDialog 里做全量扫描,把耗时操作放到工作线程里,用 PostMessage 更新 UI。
4.3 编辑后保存,文件大小变了
现象:改了几个字节,保存后发现文件比原来大了或小了几 KB。
原因:控件在保存时可能按页对齐写入,如果原始文件大小不是页大小的整数倍,末尾会补零。或者你调用了错误的保存接口,把缓冲区整个写回去了。
解决:保存前记录原始文件大小,保存后用文件系统 API 检查大小是否一致。如果控件支持“保持原始大小”选项,务必勾上。我一般会在保存后加一步校验:重新打开文件,对比修改前后的字节差异是否只在预期范围内。
4.4 中文路径下打开文件失败
现象:文件路径里有中文,OpenFile 返回失败,但路径在资源管理器里能正常打开。
原因:控件内部用的是 ANSI 文件接口,遇到 Unicode 路径就挂了。
解决:在调用 OpenFile 之前,把路径转成短路径格式(8.3 格式),或者用 WideCharToMultiByte 转成 ANSI。更稳妥的做法是把文件复制到临时目录,用纯英文路径打开,编辑完再复制回去。
4.5 多字节编辑时撤销栈溢出
现象:连续修改了几百个字节后,按 Ctrl+Z 撤销,程序崩溃。
原因:控件的撤销栈有深度限制,超过后旧记录被丢弃,但某些版本在丢弃时指针没置空,导致野指针。
解决:在应用层自己做一层撤销管理,每次编辑前把关键状态存一份快照。或者限制用户单次编辑的字节数,超过阈值就提示“建议分批保存”。
5. 进阶技巧:把 hexedit 嵌进自动化分析流水线
5.1 批量处理:用脚本驱动控件做重复编辑
如果你有一批二进制文件需要做相同的修改(比如批量替换设备序列号),手动一个个改效率太低。常见做法是写一个批处理脚本,调用你的 VC 程序,程序里用 hexedit 控件做无界面编辑。
// 无界面批量编辑模式 void BatchEdit(const CString& strFolder) { CFileFind finder; BOOL bWorking = finder.FindFile(strFolder + _T("\\*.bin")); while (bWorking) { bWorking = finder.FindNextFile(); CString strFile = finder.GetFilePath(); // 创建隐藏的 hexedit 实例 m_hexEdit.OpenFile(strFile); m_hexEdit.SetByteAt(0x1C, 0x41); // 修改某个标志位 m_hexEdit.SaveFile(); m_hexEdit.CloseFile(); } }这个模式的关键是把控件当成一个无界面的数据处理引擎,不显示窗口,只做读写。参数上,SetByteAt 的偏移和值按你的批量规则来。注意,批量操作前一定要备份原始文件,我一般会在同目录下建一个 backup 子文件夹,把原文件复制进去。
5.2 与 Python 脚本联动:用控件做可视化,用脚本做逻辑
hexedit 控件负责显示和手动编辑,复杂的解析逻辑交给 Python。常见做法是 VC 程序把当前选中的字节范围导出成 JSON,Python 脚本读取后做协议解析,结果再回传给界面显示。
# Python 侧:读取 VC 导出的选区数据 import json with open("selection.json", "r") as f: data = json.load(f) offset = data["offset"] length = data["length"] hex_bytes = data["bytes"] # 十六进制字符串 # 按协议解析,比如前4字节是长度,后面是payload payload_len = int.from_bytes(bytes.fromhex(hex_bytes[:8]), "little") payload = bytes.fromhex(hex_bytes[8:8 + payload_len * 2]) print(f"Payload length: {payload_len}, content: {payload}")这种联动方式的好处是:VC 界面负责交互,Python 负责灵活的解析逻辑,两边通过文件或管道通信。参数上,offset 和 length 由控件选区决定,bytes 是十六进制字符串,Python 侧用 bytes.fromhex 转回字节。
5.3 验证编辑结果:用 diff 工具做最终确认
改完二进制文件后,怎么确认只改了预期的地方?我一般会用 fc 命令或 Python 的 difflib 做一次对比。
fc /b original.bin modified.bin > diff.txt这个命令会逐字节对比两个文件,输出所有差异位置。如果 diff.txt 里只有你预期的那几个偏移,说明编辑干净。如果多出其他差异,说明控件在保存时动了不该动的地方,需要排查。
从那以后我每次用 hexedit 做完编辑,都强制走一遍 diff 验证,确认没有意外改动才交付。希望帮到你。
本文还有配套的精品资源,点击获取