简介:HexEdit 是一套托管于 GitHub 的开源十六进制编辑器工程源码,面向需要直接查看、搜索和修改二进制文件的开发者、逆向工程与安全分析人员,也适合游戏数值调试、损坏文件修复等字节级编辑场景。压缩包共收录 220 个文件,约 528KB,以 C 与 C++ 源文件及头文件为主体,配套 BMP、ICO、PNG 等界面位图与图标资源,并包含 Visual Studio 解决方案、项目筛选器、资源脚本、批处理构建文件及少量说明文档,便于读者梳理编辑器从界面到核心逻辑的组织方式。已有 204 人学习关注,开源属性也便于审查代码与协作改进。借助这套源码,读者可研究十六进制数据显示、字节定位与编辑、资源组织及多平台构建脚本的实现思路,也可在此基础上进行二次开发与功能扩展,对理解二进制文件分析工具的工作机制具有参考价值。
1. 二进制文件查看不是眼力活,是坐标系活
上周排查一个升级失败的问题,日志里只有一句「校验不通过」。同事把固件拖进编辑器看了两个小时,最后发现包头里那个 4 字节长度字段是小端写的,校验逻辑却按大端读——字节一个没改错,错的是读法。这类活儿最后都会落到同一件事上:二进制文件查看。工具选错,剩下全是眼力活;工具选对,剩下才是方法活。HexEdit 是 macOS 上我长期留在 Dock 里的一个原生十六进制编辑器,打开快、跳转准,几十上百 MB 的文件拖进去不会先卡你三十秒,十六进制区和字符区能对着看,改字节之前也能先把结构摸清楚。它适合经常要拆 .bin、.dat、图片头、打包产物、协议帧的人;只想读文本的没必要上它。下面按我自己的顺序拆:先定位,再建坐标系,然后才是动手改。
2. 从魔数到偏移表:一次查看拆成四步
拿到一个陌生二进制,最忌讳的就是直接打开编辑器从头滚到尾。人的肉眼对十六进制没有尺度感,滚到第 3000 行你早就忘了开头长什么样。我一般把一次查看压成四步:命令行先摸形状,编辑器里建坐标,然后把关键字段抄成表,最后才动结构视图。顺序反了,后面每一步都在返工。
2.1 先看魔数,别急着开图形界面
任何二进制文件的第一件事都是认亲。文件扩展名是给人看的,头几个字节才是它自己说的身份。.bin可能是一段裸固件,也可能压根就是个被改名的 zip;.dat更是万能后缀,什么都往里装。用file先问一句,再用xxd打印前 64 字节,两秒钟就能排除掉一半的误判。
# 1) 让系统按魔数猜类型,别信扩展名 file ./target.bin # 2) 打印前 64 字节,左列是十六进制偏移,中间是字节,右边是 ASCII xxd -l 64 ./target.bin # 3) 记录文件体量,决定后面用不用分块读 ls -lh ./target.bin-l 64表示只读 64 字节,不是读 64 行,这个参数在打开几百 MB 文件时很关键。xxd的输出左边是偏移量,默认从 0 开始、按十六进制显示,这一点必须先记住,后面所有的偏移讨论都以它为基准。
常见的头部特征值得背下来,至少背到能条件反射:
| 格式 | 起始魔数 | 关键字段偏移 | 说明 |
|---|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A | 宽高在0x10起 | 大端 |
| ZIP | 50 4B 03 04 | 压缩方法在0x08 | 小端 |
| GZIP | 1F 8B 08 | 原始大小在尾部 4 字节 | 小端 |
| BMP | 42 4D | 文件大小在0x02 | 小端 |
| PE | 4D 5A | e_lfanew在0x3C | 小端,指向 PE 头 |
| ELF | 7F 45 4C 46 | 位宽在0x04,入口在0x18 | 看EI_DATA定字节序 |
看到4D 5A就知道这不是「随便一个二进制」,而是 Windows 可执行体,接下来该去0x3C读那个 4 字节偏移;看到89 50 4E 47就知道第 9 到第 12 字节是块长度、第 13 到 16 字节是块类型。这套条件反射建立起来之后,你在编辑器里点开文件的头三秒就已经有假设了,而不是盲目地滚动。
2.2 命令行先做一次交叉验证
图形界面有个天然缺陷:你看到的是它渲染出来的样子,字体、列宽、分组方式都可能骗你。所以我习惯在打开 HexEdit 之前,先用命令行把「前 N 字节的原始值」固定下来,作为后面比对的基准。
# hexdump 的 -C 是 canonical 模式:偏移 + 16 字节十六进制 + ASCII hexdump -C -n 64 ./target.bin # od 的写法,-A x 表示偏移用十六进制,-t x1z 表示字节 + 可打印字符 od -A x -t x1z -N 64 ./target.bin # 先算一份哈希,改之前改之后都要有 shasum -a 256 ./target.binhexdump -C和xxd的输出格式几乎一样,但-C模式下遇到连续重复行会折叠成一个*,这个特性在扫大段填充区时很爽,在核对具体偏移时就是个陷阱——你以为的「第 5 行」可能实际在第 5000 行。所以核对具体字段时我改用od,它不会折叠,一行一行老实打完。
哈希这一步别省。shasum -a 256在 macOS 上是自带的,不用装东西。改之前存一份,改之后立刻再算一次,两次不一致说明写入确实生效了——这比肉眼盯着屏幕确认可靠得多,尤其是你只改了一个字节的时候。
提示:
shasum -a 256输出的是十六进制摘要,把它和文件名一起贴进你的排查记录,比截图靠谱。
2.3 在 HexEdit 里建立可复用的坐标系
进入图形界面之后,第一件事不是找字段,是调显示。默认列宽通常是每行 16 字节,这个值不是随便定的:16 是 2 的幂,一行的起始偏移永远是0x?0,心算起来最省力。如果你的字段跨在 16 字节边界上看着难受,可以临时切成 32 字节一行,但别改成 10 这种十进制友好的值,那会让所有偏移计算都变成噩梦。
第二件事是把地址栏的显示进制确认清楚。十六进制编辑器里所有地址都应该是十六进制的,但人的脑子会偷懒:看到0x100,手指在计算器上按出 100,这就已经差了 156 个字节。我的习惯是永远在纸上写带0x前缀的值,写不出前缀就先不写,逼自己养成习惯。
第三件事是把常用的跳转记成动作。看到魔数后,你已经有了第一个假设偏移,直接在跳转框里输入0x3C、0x10、0x18这类值,比用滚动条找快一个数量级。选中一段字节时,注意看状态栏给出的选区长度的单位——有的是字节数,有的是字节数加一,差一个字节在解析长度字段时足以让整个结构错位。
字符区也要用起来。很多协议帧里混着 ASCII 的标识串,比如IHDR、WAVE、ftyp,这些串在字符区一眼就能认出来,比在十六进制区数49 48 44 52快得多。我通常会把字符区宽度调窄一点,让它只在我需要的时候提供线索,不喧宾夺主。
2.4 把偏移抄成表,别记在脑子里
看完一遍之后,人会产生一种「我都记住了」的错觉。等你切去回个消息再切回来,那个0x2A到底是 payload 长度还是 flags,八成已经模糊了。所以我的规矩是:只要一个文件里出现了三个以上字段,就立刻建表。
| 字段名 | 偏移 | 长度 | 类型 | 期望值 | 实际值 |
|---|---|---|---|---|---|
| magic | 0x00 | 4 | bytes | 89 50 4E 47 | 一致 |
| ihdr_len | 0x08 | 4 | uint32 大端 | 13 | 13 |
| width | 0x10 | 4 | uint32 大端 | 1920 | 1920 |
| height | 0x14 | 4 | uint32 大端 | 1080 | 1080 |
| bit_depth | 0x18 | 1 | uint8 | 8 | 8 |
这张表的价值不在于记录,而在于「期望值」这一列。填期望值的过程会逼你回去翻规范,而翻规范的过程往往会发现你原来的假设就是错的。我见过太多次这样的情况:表填到一半,发现两个字段的偏移重叠了,那就说明前面某个字段的长度估错了,问题在动手改之前就被拦下来了。
表建好之后,HexEdit 的结构视图才有意义。先有表、后有模板,顺序不能反——模板只是把你脑子里的表用工具能懂的方式写出来,表本身错了,模板只会让你错得更自信。
3. 结构模板与字节序:让工具替你算偏移
手工数偏移这件事,做十个字段还行,做五十个字段必错。人脑不擅长累加,尤其不擅长在十六进制里累加。所以真正稳定的做法是:用代码或者模板把结构描述出来,让机器去算偏移,你只负责核对结果。这一章的核心就是把「人肉数格子」换成「声明式描述」。
3.1 人肉累加为什么会错,错在哪
先看一个典型错误。假设头部结构是这样:4 字节魔数、2 字节版本、2 字节 flags、4 字节 payload 长度、8 字节保留位。你在编辑器里从头往下数,数到 payload 长度时应该是0x08。但如果中间那个 flags 你记成了 4 字节,后面所有字段全部前移 2 字节,而且因为十六进制看起来都差不多,你未必能立刻发现。
更隐蔽的错误是「偏移 vs 序号」。第 9 个字节的偏移是0x08,不是0x09。这个差一错误在数组下标里天天发生,在二进制解析里同样天天发生。当你在编辑器里跳到0x09去读一个应该在0x08的字段,读出来的值可能依然「看起来合理」——因为二进制里到处都是00,错位读到的往往是另一个字段的高位字节。
第三种错误来自对齐。很多格式要求字段按 4 字节对齐,中间会插入填充字节。规范里写的是「保留 8 字节」,你可能理解成「8 个有意义的字节」,实际那 8 个字节全是00,纯粹为了对齐。数偏移时把填充算进去还是不算进去,结果差得很远。
结论很简单:只要字段超过五个,就别用眼睛数。要么用下面的脚本生成偏移表,要么用编辑器的结构描述功能。
3.2 用一段声明式描述替代手工数格子
我一般会先在 Python 里写一份字段清单,用struct.calcsize算出每个字段的长度和偏移,打印成表,再拿这张表去和编辑器里的实际字节对。这样做的好处是:字段定义只写一次,偏移自动累加,改了字段顺序表会自动更新。
import struct # 字段清单:(名字, struct 格式, 备注) # 格式串里的 '<' 表示小端,'4s' 表示 4 字节定长字节串 FIELDS = [ ("magic", "4s", "固定标识"), ("version", "<I", "协议版本"), ("flags", "<H", "标志位"), ("payload_len", "<I", "负载长度,不含头部"), ("reserved", "8s", "保留区,全 0"), ] offset = 0 total = 0 for name, fmt, note in FIELDS: size = struct.calcsize(fmt) print(f"{name:<12} offset=0x{offset:04X} size={size:<3} {note}") offset += size total = offset print(f"{'HEADER':<12} total={total} bytes (0x{total:02X})")这段代码跑出来是一张干净的偏移表,offset从 0 开始,每个字段的起始位置和长度都列得清清楚楚。struct.calcsize("4s")返回 4,calcsize("<I")返回 4,calcsize("<H")返回 2,calcsize("8s")返回 8——注意8s和<8s在长度上没有区别,字节串不吃端序。
参数上只有两个地方需要注意。第一,格式串里的端序字符只对数字类型生效,4s这种字节串永远按原样切,不受<或>影响。第二,calcsize在默认模式下可能会做对齐填充,如果你混用了I和H且不加端序前缀,得到的长度可能比字段实际占用更大。加上显式的<或>之后,struct会按标准大小紧凑排布,不会再插填充。
拿到这张表,再回到编辑器里逐行核对:跳到0x00应该看到magic,跳到0x04应该看到版本号。对不上就说明字段清单本身有问题,回去改清单,别改偏移。
3.3 同一段字节的两种读法:字节序必须显式声明
字节序是二进制查看里最容易「看起来没错」的坑。01 00 00 00这段字节,按小端读是 1,按大端读是 16777216。两个值都是合法整数,都不会报错,你只能靠外部信息判断哪个对。
import struct raw = bytes.fromhex("01000000") little = struct.unpack("<I", raw)[0] # 按小端解释 big = struct.unpack(">I", raw)[0] # 按大端解释 print(f"little=0x{little:08X} ({little})") print(f"big =0x{big:08X} ({big})")输出会是little=0x00000001 (1)和big=0x01000000 (16777216)。这段代码本身没什么技术含量,它的作用是逼你把「我按什么端序读的」这件事写出来。写不出来,说明你还没确定;确定了再读,出错概率下降一大截。
判断端序的依据通常有三类。一是格式规范,PNG、JPEG 这类网络格式几乎都是大端,x86 平台上的私有结构体基本都是小端。二是数值合理性,比如长度字段读出来是 16777216 而文件总共才 3 MB,那一定是端序反了。三是魔数本身,有些格式会在头部放一个固定的校验值,用两种端序各读一遍,哪个等于规范里的常量就是哪个。
在编辑器里看的时候,还有一个辅助手段:把同一段字节分别按 1、2、4、8 字节宽度切换显示,看哪个宽度的值「像个正常的数」。这招不严谨,但在没有规范文档的时候能快速收敛假设。
3.4 结构描述落到编辑器里,只做一件事
HexEdit 这类工具支持把结构描述出来,在十六进制视图旁边叠一层字段名和解析值。我自己的用法很克制:只用它做「可视化核对」,不用它做「自动生成结论」。
具体做法是,把上一节算出来的偏移表按顺序写成结构描述,字段名和脚本里的名字保持完全一致,长度和端序也保持一致。然后在编辑器里加载这个描述,看它解析出来的值和 Python 解析出来的值是否一一对应。对应上了,说明两边理解一致;对应不上,先怀疑结构描述写错了,再怀疑文件本身有问题。
这里有个容易忽略的点:结构描述里的字段顺序必须和文件里的物理顺序一致,不能按逻辑顺序写。有些格式的逻辑顺序和物理顺序是打乱的,比如头部在文件末尾、索引在文件开头。这种时候结构描述反而会误导你,老老实实用偏移表加跳转更稳。
另一个点是嵌套。结构里套结构的时候,偏移是相对的还是绝对的,各家工具的处理不一样。我的习惯是全部拍平成绝对偏移,宁可多写几行,也不去赌工具的嵌套语义。拍平之后,任何一行出错都能单独定位,不用顺藤摸瓜。
4. 大文件与写入安全:动手改字节前的三道闸
看到这里都还是「读」。真正危险的是「写」。十六进制编辑器里一次误操作,可能把整个文件的结构毁掉,而且很多编辑器不提供可靠的撤销。我给自己定了三道闸:备份加哈希、副本策略、回读校验。三道都过,才允许在主文件上落笔。
4.1 第一道闸:备份与哈希,先给自己留后悔药
备份这件事听起来废话,但我见过太多「就改一个字节,不用备份」的现场。改一个字节确实可能没事,但改一个字节之前你得先确认这个字节在哪,而确认的过程本身就可能误触。
#!/usr/bin/env bash set -euo pipefail SRC="firmware.bin" STAMP="$(date +%Y%m%d%H%M%S)" BAK="${SRC}.${STAMP}.bak" # -p 保留时间戳和权限,某些固件工具会检查 mtime cp -p "$SRC" "$BAK" # 原始文件、备份文件各算一次,两次必须一致 echo "--- 原始 ---" shasum -a 256 "$SRC" echo "--- 备份 ---" shasum -a 256 "$BAK"set -euo pipefail这三件套是必须的:-e让任何一步失败就停,-u让未定义变量直接报错(防止$SRC拼错变成空串把根目录搞了),-o pipefail让管道里任何一环失败都能被捕获。cp -p的-p保留权限和时间戳,有些工具链会拿 mtime 做增量判断,时间戳变了会触发不必要的全量重建。
两次哈希输出必须逐字符一致。如果不一致,别急着往下走,先看看是不是复制过程中源文件被别的进程写了。
提示:备份文件不要放在同一个会被批量清理的临时目录里。我一般放在同级的
backup/下,命名带时间戳,一个项目最多留五份,多了就删最老的。
4.2 第二道闸:只在副本上改,主文件最后才碰
我的实际流程是:主文件只读,所有修改先在副本上做,验证通过后再用副本覆盖主文件。这样即使副本改废了,主文件还是干净的,重来一次的成本只是复制一份。
在编辑器里打开文件时,如果工具提供了只读模式,先勾上。只读模式下你能随便跳、随便选、随便看结构,就是按不了保存。等确认了要改的偏移和期望值,再关掉只读。这一步听起来多余,但它把「探索」和「修改」两个阶段物理隔开了,误触概率下降非常明显。
副本命名也要有规矩。我一般用xxx.work作为工作副本,改完验证通过后mv成正式文件名。这样从文件名就能看出哪个是原始、哪个是半成品、哪个是成品,不用靠记忆。
4.3 第三道闸:带期望值的写入,写完立刻回读
真正落笔的时候,我不用编辑器的直接编辑功能,而是写脚本。原因很简单:脚本可以在写入前检查「这个位置现在是不是我以为什么」,不匹配就中止。
import hashlib import os import shutil from pathlib import Path def sha256(path: Path) -> str: h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def patch(path: str, offset: int, expect: bytes, new: bytes) -> None: p = Path(path) bak = p.with_name(p.name + ".bak") if not bak.exists(): shutil.copy2(p, bak) with open(p, "r+b") as f: f.seek(offset) cur = f.read(len(expect)) if cur != expect: raise SystemExit( f"偏移 0x{offset:X} 实际为 {cur.hex()}," f"预期 {expect.hex()},已中止" ) f.seek(offset) f.write(new) f.flush() os.fsync(f.fileno()) # 强制落盘,别信缓冲 # 回读校验:重新打开文件读一遍,确认真的写进去了 with open(p, "rb") as f: f.seek(offset) back = f.read(len(new)) if back != new: raise SystemExit(f"回读不一致:{back.hex()} != {new.hex()}") print(f"OK offset=0x{offset:X} {expect.hex()} -> {new.hex()}") print(f"sha256={sha256(p)}") if __name__ == "__main__": patch("firmware.work", 0x1F4, b"\x00\x00\x00\x01", b"\x00\x00\x00\x02")几个参数值得说明。expect是写入前必须匹配的原始字节,长度必须和new一致,不一致会导致后续偏移全部错位。os.fsync在 macOS 上会把内核缓冲刷到磁盘,不加这一句,脚本正常退出但断电后文件可能是旧的。回读用的是新打开的文件句柄,不复用写入时的句柄,避免读到缓存。
shutil.copy2和第二道闸里的cp -p效果类似,保留元数据。备份只在不存在时创建,避免重复运行脚本时把已经改过的版本覆盖成备份。
4.4 大文件不要整读进内存,分块扫就够了
几十 MB 的文件用read_bytes()无所谓,上 GB 的时候就会开始吃内存甚至被系统杀掉。做统计类操作时我改成流式读取。
from collections import Counter def scan_freq(path: str, block: int = 4 << 20) -> None: freq = Counter() total = 0 with open(path, "rb") as f: for chunk in iter(lambda: f.read(block), b""): freq.update(chunk) total += len(chunk) print(f"total={total} bytes") for byte, cnt in freq.most_common(6): print(f"0x{byte:02X} count={cnt:<12} ratio={cnt / total:.2%}") if __name__ == "__main__": scan_freq("firmware.bin")block设成 4 MB 是个经验值:太小了系统调用次数多,太大了单次内存占用高。iter(lambda: f.read(block), b"")是标准的流式读法,读到空字节串就停,不会漏掉最后一块不完整的块。Counter.update直接吃 bytes 对象,会按字节逐个计数,比自己写循环快。
这个统计能回答不少问题:如果 0x00 占比超过 60%,说明文件里有大量填充或未使用区,真正有效的数据可能集中在某几段;如果 0xFF 占比异常高,可能是擦除态没写过的闪存镜像。这些判断不精确,但能帮你快速缩小要重点看的范围。
5. 排查:二进制文件查看里最容易翻车的五个瞬间
下面这些都是我自己或者身边人真实翻过的车,按「现象 → 原因 → 解决」写,能对上号最好,对不上也当个检查清单。
5.1 写入与备份环节的三次翻车
现象一:改完保存,工具打开报格式错误,撤销也回不去。
原因是直接在主文件上编辑,而且编辑器的撤销栈只覆盖当前会话,一旦保存并关闭,历史就没了。有些编辑器会生成一个临时文件,但那个文件在正常退出时会被清理。解决办法只有一个:动手前先复制一份带时间戳的备份,用cp -p而不是cp,保留原始 mtime,避免某些构建系统因为时间戳变化触发全量重编。我现在已经养成条件反射,手指还没碰到键盘,备份命令已经在终端里跑完了。
现象二:脚本写入成功,但设备读出来还是旧值。
原因是写入走了页缓存,脚本退出时数据还在内核缓冲里,没真正落盘。表现是本地xxd看是新值,换个工具或者重启后再看又变回去了。解决是在write之后调用flush()加os.fsync(fileno()),两个都要,只flush只是把 Python 层的缓冲推到操作系统,fsync才推到磁盘。如果是往块设备或者挂载的镜像里写,还要考虑sync命令。
现象三:备份文件被当成正式文件用了。
原因是备份命名和主文件太像,比如firmware.bin.bak和firmware.bin,在文件管理器里排序后挨在一起,拖错了自己都没发现。表现是改了半天没效果,其实是改在备份上。解决是备份放到独立子目录,或者命名里带明显的时间戳前缀,让它在排序上远离主文件。我现在的习惯是backup/20240101-120000_firmware.bin,一眼就能看出这不是当前工作文件。
5.2 判断与读值环节的两次误判
现象四:字段值读出来是个巨大的数,明显不合理。
原因是字节序搞反。01 00 00 00按小端是 1,按大端是 16777216,如果你的文件只有几 MB,那 16777216 这个值本身就是个报警信号。解决是用两种端序各读一遍,拿物理约束去筛:长度字段不能大于文件大小,宽高不能是几万,版本号通常是个位数或两位小数。还有一种情况是偏移差了一位,读到的其实是相邻字段的高位字节,这种时候值往往「看起来还行」,最难发现,所以我在读任何关键字段前都会先打印它前后各 8 字节的原始内容做对照。
现象五:hexdump -C的输出和编辑器里对不上。
原因是hexdump -C会把连续重复的行折叠成一个*,你以为的第 20 行实际在第 2000 行。表现是照着命令行的偏移去编辑器里跳,跳过去看到的内容完全无关。解决办法是核对具体偏移时改用od -A x -t x1z,它不做折叠;或者给hexdump加上-v参数关掉折叠。这个小坑我踩过不止一次,现在只要涉及「按行号定位」,一律不用hexdump。
5.3 一份可以贴在显示器边上的自查清单
每次动手前花三十秒过一遍:备份做了没有,哈希存了没有,改的是副本还是主文件,期望值写进脚本了没有,端序显式声明了没有,改完回读验证了没有。这六条里任何一条没做到,就先别按保存。
还有一条不在清单里但同样重要:把「我为什么认为这个偏移是对的」写下来。写不出理由的假设,八成是猜的。猜出来的偏移,改之前多验证一次,成本远低于改完之后回滚。
6. 进阶:把一次查看沉淀成可复用的解析脚本
单次排查做完,我的习惯是把偏移表、校验逻辑、写入函数合成一个脚本,参数化之后留在项目仓库里。下一次遇到同类文件,--info看结构、--check做校验、--patch做修改,三分钟就能复现上一次的全部分析。
具体做法是拆成三个子命令:info打印头部字段和偏移表,check做完整性校验(比如块级 CRC),patch执行带期望值比对的写入。下面这段是 PNG 场景的示意,其他格式换掉字段定义和校验函数就行。
import argparse, struct, zlib from pathlib import Path def read_chunks(path): raw = Path(path).read_bytes() assert raw[:8] == b"\x89PNG\r\n\x1a\n", "签名不符" off, out = 8, [] while off < len(raw): length, = struct.unpack_from(">I", raw, off) ctype = raw[off + 4:off + 8].decode("ascii", "replace") data = raw[off + 8:off + 8 + length] crc, = struct.unpack_from(">I", raw, off + 8 + length) out.append((off, ctype, data, crc)) off += 12 + length if ctype == "IEND": break return out def cmd_info(args): for off, ctype, data, crc in read_chunks(args.path): calc = zlib.crc32(ctype.encode() + data) & 0xFFFFFFFF flag = "OK" if calc == crc else f"BAD(calc=0x{calc:08X})" print(f"0x{off:06X} {ctype:<6} len={len(data):<8} crc={flag}") parser = argparse.ArgumentParser() sub = parser.add_subparsers(required=True) p_info = sub.add_parser("info") p_info.add_argument("path") p_info.set_defaults(func=cmd_info) args = parser.parse_args() args.func(args)关键在于把 CRC 校验放进info里一起输出。很多时候文件「能打开」但某一块 CRC 对不上,肉眼完全看不出来,只有算一遍才知道。zlib.crc32返回的是无符号值,和文件里存的 4 字节大端整数比较前要& 0xFFFFFFFF,否则在某些版本上会得到负数。
一个具体的技巧:把这份脚本和备份命令写进同一个Makefile,make backup和make info FILE=xxx各占一行。人 memorizing 命令的能力远不如记忆动词,make info这种短动作能坚持下来,长命令坚持不下来。
从那以后,我每次打开一个陌生二进制之前,都会强制走一遍「file → xxd 前 64 字节 → 哈希备份 → 偏移表」这四步,哪怕只是一个 4 KB 的配置文件。血泪经验是,省掉任何一步省下来的两分钟,往往会在后面以两小时的形式还回来。希望帮到你。
本文还有配套的精品资源,点击获取