资讯动态

HexEdit 十六进制编辑器:源码编译、文件头解析与改字节验证

发布时间:2026/10/29 20:10:35 来源:尧图企业网站定制
简介HexEdit 是一份托管于 GitHub 的开源十六进制编辑器提供完整源码工程面向需要查看、搜索与修改二进制文件的开发者、逆向工程师、安全研究人员及游戏修改爱好者可用于字节级编辑、数据模式查找、可执行文件内容排查与损坏文件修复等场景。压缩包约 528KB共 220 个文件以 C/C 源码为主体51 个 c、48 个 cpp 与 47 个 h 构成编辑器核心逻辑与数据结构配套 8 个 vcxproj、2 个 sln 等工程文件便于在 Visual Studio 中直接编译另有 18 个 bmp、10 个 ico、4 个 png 与 3 个 rc 资源脚本承载十六进制视图和工具栏界面bat 脚本辅助构建。已有 204 人学习下载。工程目录清晰核心逻辑、界面资源与构建配置彼此分离读者既能编译出可用工具也能研读十六进制显示、字节定位与文件缓冲读写的实现思路为二次开发或安全分析改造提供可复用基础。1. 从一次「文件头对不上」说起HexEdit 能替你解决什么排查一个打不开的资源包时最有效的动作往往不是加日志而是把文件按字节摊开。二进制文件查看这类活文本编辑器干不了命令行工具看得到却改不动HexEdit 正好卡在两者中间它是 macOS 上的开源十六进制编辑器左侧偏移、中间十六进制、右侧 ASCII 三栏对齐支持按偏移跳转、十六进制查找替换、直接改字节并写回原文件仓库给的是完整可编译的 Xcode 工程不是一个打包好的安装镜像。它适合三类人改固件配置、拆自定义文件格式、核对协议报文的开发者也适合想读一份真实 AppKit 文本渲染代码的人。只改文本配置的话用不上它。2. 从源码到能双击的 app工程拆解与三次编译尝试只有源码仓库的 macOS 工具第一件让人翻车的事往往不是不会用而是压根编译不出来。工程里带着原作者的开发环境假设部署版本、SDK 版本、签名方式任意一项和你机器上的 Xcode 不匹配报错都会指向一个和真实原因无关的位置。我一般按「先看清结构、再命令行编译、最后处理产物签名」三段落走每段有独立的失败面拆开处理比一上来就按 CmdR 好排查得多。2.1 仓库里到底有什么三类文件分开看HexEdit 是标准的 Xcode 工程布局没有把依赖摊成一堆子模块这对编译是好事但也意味着所有问题都集中在工程配置本身。拿到仓库先别急着打开用两条命令把结构摸清楚工程文件在哪一层、源码目录有多少个头文件和实现文件、Info.plist 是独立文件还是被写进了 Build Settings 里。# 进入仓库根目录后先定位工程文件与源码分布 ls -la find . -maxdepth 3 -name *.xcodeproj -print find . -maxdepth 3 \( -name *.m -o -name *.h \) | head -40 # 看看工程里到底有几个 target、哪些 scheme 可以直接构建 xcodebuild -list -project HexEdit.xcodeproj第二条命令是关键。-list会列出 Target、Build Configurations 和 Schemes 三块信息。Scheme 存在说明可以直接用-scheme参数构建如果 Schemes 为空就得退回-target方式否则后面那条xcodebuild会直接报「scheme 不存在」而错误信息里并不会提醒你其实是这个原因。类别典型内容要不要动工程配置.xcodeproj、scheme、Build Settings只在签名、部署版本报错时动改多了连锁崩源码Objective-C 的.h/.m视图与控制器想加功能、改界面才动资源与描述Info.plist、图标、本地化字符串改应用名、图标、包标识时才动一个血泪经验不要一上来就把 Deployment Target 拉到当前系统版本。老工程的 AppKit 调用可能依赖较旧的 API 行为拉高之后编译是过了运行时窗口渲染或文本控件的行为会变。先按工程原有设置编一次跑起来看效果再决定要不要动。2.2 一条 xcodebuild 命令走通编译图形界面里点构建报错信息会散落在 Issue Navigator 里复制给别人很麻烦。命令行构建的好处是输出是线性的出错位置一目了然也方便你把命令贴进脚本反复跑。下面这条是我常用的最小可用组合# 在仓库根目录执行把输出目录固定到 ./build方便找产物 xcodebuild \ -project HexEdit.xcodeproj \ -scheme HexEdit \ -configuration Release \ -derivedDataPath ./build \ CODE_SIGN_IDENTITY- \ CODE_SIGNING_REQUIREDNO \ CODE_SIGNING_ALLOWEDNO \ build逐项说明一下这几个参数为什么这么写。-project指定工程文件工程不在根目录时要写相对路径。-scheme必须和xcodebuild -list里列出的名称完全一致大小写都敏感。-configuration Release走优化编译产物体积小、启动快排查问题时换成Debug崩溃堆栈里的符号更完整。-derivedDataPath ./build把中间产物和最终 app 都收进仓库目录下避免去~/Library/Developer/Xcode/DerivedData那一堆哈希目录里翻。后面三个签名开关是给没有开发者证书的机器准备的。CODE_SIGNING_ALLOWEDNO关掉整个签名阶段CODE_SIGNING_REQUIREDNO防止某些 target 强制要求签名CODE_SIGN_IDENTITY-是兜底含义是使用 ad-hoc 签名而不是某个具体证书。这三者建议一起写只写其中一个经常还是会卡在requires a development team上。构建成功后产物路径是# 找到 app 包确认目录结构完整 ls -la build/Build/Products/Release/ # 直接运行看它能不能起来 open build/Build/Products/Release/HexEdit.app如果Build/Products/Release/下只有.app而没有对应的.dSYM说明调试符号没生成这不影响使用但之后真崩溃了就没有符号化的堆栈Debug 配置下编一次就能补上。2.3 产物落地ad-hoc 签名与隔离属性编译出来的 app 直接双击很可能弹一个「无法验证开发者」的拦截。这不是编译失败而是系统的 Gatekeeper 在起作用。自编译的产物没有经过公证用 ad-hoc 签名加一次本地信任就能绕过去APPbuild/Build/Products/Release/HexEdit.app # 1) 清掉可能被继承下来的扩展属性下载来的仓库可能带隔离标记 xattr -cr $APP # 2) 用 ad-hoc 身份重签--deep 让内嵌的 framework / helper 一起签 codesign --force --deep --sign - $APP # 3) 校验签名是否自洽 codesign -dv --verbose4 $APP 21 | head -20 codesign --verify --deep --strict --verbose2 $APPcodesign --sign -里的那个短横线就是 ad-hoc 身份的写法不是占位符别漏。--force允许覆盖已有签名--deep会递归处理 bundle 内的嵌套组件——macOS 上带 helper 的 app 如果不加这个参数主程序签过了、helper 还是旧的启动时照样被拦。codesign -dv --verbose4的输出里重点看Identifier、TeamIdentifier和Signature三行ad-hoc 签名时TeamIdentifier显示为not set这是正常的。spctl -a -vv $APP在 ad-hoc 签名下大概率返回rejected别拿它当判断标准能正常启动就是过。真正需要处理的是每次重新编译后签名会失效所以这条命令得跟着构建脚本一起跑别指望签一次永久有效。3. 十六进制视图的读法偏移坐标、字节序与字段宽度十六进制编辑器真正的门槛不在「看得懂 0-F」而在把一堆两位十六进制数还原成有意义的字段。同一个字节序列按不同宽度、不同字节序去读能得到完全不同的结果而界面上没有任何提示告诉你该按哪种读。这一章把这三件事拆开偏移怎么算、字节序怎么判、字段宽度怎么定最后拿一个我自己造的模拟包做一遍完整解析。3.1 偏移是唯一坐标行号不是HexEdit 的十六进制区左边那一列是偏移量十六进制显示通常从00000000开始每行 16 字节。这一列是你在文件里唯一可靠的坐标行号和列号都只是它的视觉投影——换个窗口宽度、改一下每行字节数设置同一个字节就会跑到别的行去偏移不会变。日常最容易出错的是「知道偏移但算不出来」。比如查找结果告诉你某字段在偏移0x1A4你想知道它属于第几行第几列# 十六进制偏移与行列的换算每行 16 字节 # 行号 offset / 16列号 offset % 16 printf row%d col%d\n $((0x1A4 / 16)) $((0x1A4 % 16))结果应该是第 26 行、第 4 列从 0 开始数。这个换算看着简单但当你在一堆嵌套结构里跳来跳去时靠心算迟早出错用命令或者随手写个小函数更稳。查找是另一处高频坑。十六进制查找必须按十六进制输入比如要找4D 5A有些工具接受4D5A有些接受带空格的4D 5A还有些会把输入当成 ASCII 字符串去搜4D5A这四个字符。搜不到的时候先确认模式再确认大小写最后才怀疑文件里真的没有。3.2 字节序决定你把 08 00 00 00 读成 8 还是 134217728四个字节08 00 00 00小端读法是 8大端读法是 134217728。这个差异足够让一个长度字段从「合理」变成「明显错误」也是新手看十六进制最常卡住的地方。判断字节序没有捷径但有经验法则格式规范里通常会说清字段的字节序没有规范的私有格式就看相邻字段能不能读出合理值。比如一个文件头的魔数后面跟着版本号如果是02 00小端读成 2、大端读成 512前者明显更像版本号那这个格式大概率是小端。同一份文件里字节序通常一致判断出一个字段其余照着来就行。字段宽度小端示例小端值大端值常见用途uint1602 002512版本号、标志位uint3208 00 00 008134217728长度、偏移、条目数uint3200 10 00 0040961048576区块大小、对齐单位uint6400 04 00 00 00 00 00 0010241125899906842624时间戳、大文件偏移表里最后一行顺带说明一件事宽度选错比字节序选错更难发现。uint64 的位置按 uint32 读只能读到低 32 位剩下四个字节会被你当成下一个字段的开始后面整个结构全部错位。所以我拆新格式时的固定动作是先把前 64 字节原样抄进笔记标上每个字节的偏移再一个字段一个字段往上套套错了好回头。3.3 拆一个模拟包的文件头从 0x00 到 0x20下面用一个我自己造的模拟包格式就叫它模拟项目X的容器格式走一遍完整流程。假设 HexEdit 里打开后前 32 字节长这样00000000 4D 50 4B 31 02 00 20 00 10 00 00 00 04 00 00 00 |MPK1.. .........| 00000010 A4 1B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|按小端逐字段读偏移0x00四个字节是魔数MPK10x04两个字节版本号02 00读作 20x06两个字节头部长度20 00读作 32正好覆盖到0x200x08四个字节是条目数读作 160x0C四个字节是数据区起始偏移读作 4——这里就不对了数据区不可能从第 4 字节开始说明我对这个位置的假设是错的它更可能是个标志位或者索引表长度。0x10四个字节A4 1B 00 00读作 7076像是数据区长度。把假设写成代码去验证比在界面上反复横跳快得多import struct # 按假定的模拟包头部结构逐字段解析 FMT 4sHHII I # 表示小端I 之间留空格只为可读 HEADER_SIZE 0x20 with open(sample.mpk, rb) as f: raw f.read(HEADER_SIZE) magic, ver, hdr_len, entry_cnt, field_0c, data_len struct.unpack(FMT, raw[:20]) print(fmagic {magic!r}) # 期望 bMPK1 print(fversion {ver}) # 期望 2 print(fheader_len {hdr_len}) # 期望 32等于 HEADER_SIZE 说明自洽 print(fentries {entry_cnt}) # 条目数 print(ffield_0x0c {field_0c}) # 存疑字段看它是否随文件变化 print(fdata_len {data_len}) # 自洽性检查头部长度 数据长度应该接近文件总大小 import os total os.path.getsize(sample.mpk) print(ffile_size {total}, 差值 {total - hdr_len - data_len})struct的格式串是整个脚本的核心。表示小端改成就是大端其它字符不用动这一条就能帮你快速验证字节序假设对不对。4s读四个字节的原生字节串H是无符号 16 位I是无符号 32 位格式串里的空格会被忽略加空格纯粹是为了和偏移对得上眼。最后那段自洽性检查是我每次拆新格式都必做的一步如果头部长度 数据长度和文件实际大小差得离谱说明字段解释错了别急着往下拆。提示把这段脚本存成parse_header.py每次改字段假设就改格式串比在 HexEdit 里点来点去快十倍。HexEdit 负责看原始字节和改字节结构解释交给脚本两者配合才是最省事的组合。4. 改字节的连锁反应写回策略、校验和与批量替换看得懂字节只是第一步真正出问题的是「改」。十六进制编辑器不像配置文件编辑器它不会在保存时帮你做任何校验字节写进去了就是写进去了文件结构是否还自洽、校验和是否还对得上、上层程序是否还认全得你自己负责。这一章讲的是我在改二进制文件时固定遵守的几条规矩。4.1 先备份再动手写回路径与只读文件HexEdit 这类编辑器默认是直接写回原文件不像某些工具会强制「另存为」。这个设计对效率有好处对事故率不友好——一次误操作覆盖掉几个字节没有撤销历史的话基本没法回滚。# 动手前先留一份原始副本带上时间戳别用 .bak 这种会被覆盖的名字 cp sample.mpk sample.mpk.$(date %Y%m%d%H%M%S).orig # 确认文件权限只读文件在编辑器里会保存失败但提示不一定明显 ls -l sample.mpk chmod uw sample.mpk # 需要写入时再放开 # 记录原始校验值改完之后用来确认差异范围符合预期 shasum -a 256 sample.mpkshasum -a 256那条是我强烈建议保留的习惯。改完之后再算一次如果哈希没变说明你的保存根本没生效常见于只读文件或者改的是副本省得你在上层应用里排查半天。如果哈希变了但你只想改 4 个字节用第 6 章讲的字节级 diff 确认改动范围别让编辑器的批量替换悄悄动了别处。另外一个细节HexEdit 打开大文件时的行为和文件是否被其它进程占用有关。改之前先确认没有别的程序正在读写这个文件尤其是模拟器、打包工具这类会持续持有文件句柄的进程边跑边改很容易出现「保存成功但内容回滚」的玄学现象。4.2 校验和与长度字段改一个字节要连带改三处二进制格式里最常见的结构是「长度 数据 校验和」。你改了数据区的一个字节长度没变、校验和却对不上了上层程序要么直接拒绝加载要么读到一半报错。所以每次改数据都要问自己三个问题有没有长度字段依赖它有没有校验和覆盖它有没有偏移表指向它以第 3 章那个模拟包为例假设0x10处的 4 字节确实是数据区 CRC32覆盖范围是0x20之后的全部内容。改完一个字节后重算校验并回填import struct, zlib PATH sample.mpk CRC_OFFSET 0x10 # 校验和字段自身的偏移 DATA_START 0x20 # 被校验数据的起始偏移 with open(PATH, rb) as f: f.seek(0) buf bytearray(f.read()) payload buf[DATA_START:] crc zlib.crc32(payload) 0xFFFFFFFF # 小端写回 4 字节若格式是大端把 I 换成 I struct.pack_into(I, buf, CRC_OFFSET, crc) f.seek(0) f.write(buf) f.truncate() # 防止原文件更长时留下尾部垃圾 print(fCRC32 已回填: 0x{crc:08X})几个容易忽略的点。zlib.crc32返回的是有符号结果在某些 Python 版本上可能是负数所以必须 0xFFFFFFFF归一化否则pack_into会直接抛异常。f.truncate()是为了应对「新内容比原文件短」的情况——如果只write不truncate文件尾部会残留旧数据长度字段和实际大小就对不上了。另外 CRC 的覆盖范围是最容易搞错的地方是包含头部还是只覆盖数据区、是否包含校验字段自身差一个字节算出来的值就完全不同这个只能靠反复试或者看格式文档确认。如果格式用的是长度字段而不是校验和逻辑反过来先改数据再回头把长度字段更新成新的字节数注意单位是字节还是条目数——这两者差一个倍数是另一个高频翻车点。4.3 批量修改别用编辑器脚本打补丁更稳HexEdit 是交互式工具适合精确到几个字节的定点修改。一旦涉及「把文件里所有0x00 0x01换成0x00 0x02」这种批量操作就别在编辑器里手动重复了既容易漏又没法复现出了问题连自己改过哪些位置都说不清。批量改写的正确姿势是写补丁脚本把「改哪里、改成什么」变成可review、可重跑的代码import struct PATCHES [ # (偏移, 格式, 新值) —— 定点补丁表一行一个改动 (0x04, H, 3), # 版本号 2 - 3 (0x08, I, 32), # 条目数 16 - 32 (0x0C, I, 0x200), # 数据区起始偏移 ] with open(sample.mpk, rb) as f: for off, fmt, val in PATCHES: f.seek(off) old f.read(struct.calcsize(fmt)) f.seek(off) f.write(struct.pack(fmt, val)) print(f0x{off:04X}: {old.hex()} - {struct.pack(fmt, val).hex()})这种写法的价值在于补丁表本身就是文档别人看这十几行就知道你改了什么你自己过两周也能一眼看懂。struct.calcsize(fmt)保证读写的宽度和格式串一致不会出现「写 2 字节但读 4 字节」的错位。最好再加一条断言验证写入前的旧值符合预期——万一文件版本不对脚本会立刻报错而不是默默改坏。5. 常见问题排查编译失败、文件打不开、改完不生效HexEdit 这类工具出问题的场景很集中基本落在三段编译不通过、文件打不开或显示异常、改完不生效。这三段的排查路径完全不同混在一起查最容易浪费时间。下面按段整理我实际遇到过的情况每条按现象、原因、解决三步写。5.1 构建与签名阶段现象一xcodebuild报Signing for HexEdit requires a development team明明本地没有任何证书。原因是工程开了自动管理签名Xcode 认为你需要一个真实团队 ID 才能签。解决办法是把签名阶段整个关掉而不是去纠结证书在命令里加CODE_SIGNING_ALLOWEDNO CODE_SIGNING_REQUIREDNO或者打开工程在 Signing Capabilities 里取消勾选 Automatically manage signing、把 Team 设为 None。我一般两种都做命令行和图形界面共用一套配置省得下次又踩。现象二编译过了双击 app 提示「无法打开因为无法验证开发者」。这不是编译问题是 Gatekeeper 对未公证产物的拦截。解决是第 2.3 节那套xattr -cr清扩展属性codesign --force --deep --sign -做 ad-hoc 重签。注意每次重新编译都要重签把它写进构建脚本的最后一行。现象三xcodebuild -list输出的 Schemes 是空的-scheme参数直接报错。原因是工程没有共享 schemescheme 默认存在用户目录下不进版本库。解决是改用-target参数构建或者在 Xcode 里打开工程在 Manage Schemes 里勾选 Shared 之后再执行命令行构建。5.2 打开与显示阶段现象四打开几百 MB 甚至上 GB 的文件界面长时间无响应转圈之后可能直接崩。原因是编辑器需要把内容读进内存并渲染成行文件越大行数越多渲染开销线性上升。解决是先切片段再分析用dd把头尾或目标区间抠出来单独看需要精确定位时用xxd -s从指定偏移开始看别指望一次性把整个大文件塞进界面。# 从 0x1000 开始截取 4KB单独丢给编辑器看避免整文件加载 dd ifbig.bin ofchunk.bin bs1 skip$((0x1000)) count4096 # 不落盘直接从指定偏移看 64 字节 xxd -g 1 -l 64 -s 0x1000 big.binbs1配上skip是按字节精确定位文件特别大时慢可以用bs4096 skipN换算提速。xxd的-g 1是按单字节分组显示默认 2 字节一组-l限长度-s定起点这三个参数组合起来相当于一个只读的、不占内存的十六进制查看器。现象五查找十六进制值搜不到但用xxd看明明存在。原因是输入被当成了 ASCII 文本。解决是切到十六进制查找模式注意是否需要空格分隔、大小写是否敏感实在不确定就先搜一段短的两个字节确认命中再逐步加长。5.3 修改与写回阶段现象六改完字节保存了重新打开发现内容恢复原样。原因通常是文件只读或者你改的是打开时载入的副本而不是原文件。解决是先ls -l确认写权限再用shasum对比改前改后的哈希哈希没变就是没写进去。现象七改完保存成功上层程序仍然报错。这条最常见也最容易被误判为「编辑器坏了」。真实原因几乎总是校验和或长度字段没同步更新或者文件本身带签名、改动后被签名校验拒绝。解决是按第 4.2 节的思路把依赖这次改动的字段全部重算一遍再检查文件末尾有没有附加的校验区。6. 进阶把 HexEdit 当结构分析器用以及三步验证法拆二进制拆到一定数量之后你会发现瓶颈不在「看不懂字节」而在「记不住偏移」。每次打开文件都要重新数一遍哪个字段在第几个字节改一行结构就得全部重算。我的做法是把结构定义固化成模板脚本HexEdit 只负责两件事看原始字节、做定点修改解析、校验、比对全部交给脚本。这个分工一旦建立起来拆新格式的时间能砍掉一大半。6.1 用模板脚本替代肉眼数偏移模板的核心是一张字段表每个字段写清楚名字、偏移、宽度、字节序和含义。这张表可以直接驱动解析、也可以反过来驱动补丁生成import struct # 字段表名字 - (偏移, struct 格式, 说明) FIELDS { magic: (0x00, 4s, 文件标识), version: (0x04, H, 格式版本), header_len: (0x06, H, 头部字节数), entry_cnt: (0x08, I, 条目数量), data_off: (0x0C, I, 数据区起始偏移), data_len: (0x10, I, 数据区长度), } def parse(path): with open(path, rb) as f: head f.read(0x20) out {} for name, (off, fmt, _) in FIELDS.items(): size struct.calcsize(fmt) out[name] struct.unpack(fmt, head[off:off size])[0] return out if __name__ __main__: info parse(sample.mpk) width max(len(k) for k in info) for k, v in info.items(): print(f{k:{width}} {v!r} ({FIELDS[k][2]}))好处有三点字段表本身就是格式文档比散落在笔记里的截图可靠换字节序只需要改格式串里的一个字符同一个脚本可以直接跑批量文件做一致性统计——比如扫一百个样本看version是不是只有 2 和 3 两个取值这种统计能帮你确认字段解释对不对。配合 HexEdit 使用的方式是脚本解析出可疑字段回到编辑器跳转到对应偏移肉眼确认那几个字节的原始值再回来改假设。两边互相验证比单靠任何一边都快。6.2 三步验证字节 diff、校验、真实加载改完文件别急着交付我固定走三步验证少一步都可能在上层踩坑。验证手段能发现什么发现不了什么字节级 diff是否只改了预期位置、有没有误改改动逻辑是否正确哈希/校验和比对校验字段是否同步、文件是否完整字段语义是否成立上层程序真实加载格式是否自洽、功能是否正常极端输入下的边界行为第一步用cmp和xxd对照重点看「差异数量是否等于预期」——说好改 4 个字节diff 出来 40 处差异那就是编辑器或脚本出了问题# 列出所有差异字节的位置1-based输出为八进制旧值/新值 cmp -l before.mpk after.mpk | head -20 # 同一区间并排看确认改动前后的原始字节 xxd -g 1 -l 32 -s 0x00 before.mpk xxd -g 1 -l 32 -s 0x00 after.mpkcmp -l输出的是差异位置和两个字节值注意它按八进制打印十进制读惯了的话第一眼会算错。xxd那两条命令的作用是把差异落到肉眼可见的位置上尤其是字段边界附近改动时能立刻看出有没有越界覆盖到下一个字段。第二步重算校验和并和文件里的值对上第三步不做任何绕过直接用正常路径加载——包管理器照常安装、模拟器照常启动、解析库照常解析。前两步只能证明「你的假设自洽」第三步才能证明「你的改动被真实消费方接受」。有个教训我记到现在曾经因为跳过了第一步把校验和字段写回了正确值却顺手覆盖了相邻的一个保留字段前两步全过最后在上层加载时报了一个完全不相干的错误查了整整一个下午。从那以后我每次改二进制文件都强制走一遍「备份、定点补丁、字节 diff、校验重算、真实加载」这五步一步不省。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑