ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

bin转txt工具:十六进制转储与结构化解析实战

bin转txt工具:十六进制转储与结构化解析实战 简介这是一款面向软件开发者、数据分析师与系统管理员的二进制转文本实用工具专门解决bin文件难以直接阅读与解析的问题。工具基于Visual Studio 2010开发支持处理任意大小的bin文件可将原始字节数据解码为可读文本或十六进制形式适用于系统日志分析、固件数据查看等场景。压缩包共78个文件约6.36MB包含2个cpp与2个h源文件、1个sln解决方案及vcxproj工程文件另有2个exe可执行程序、4个bin测试样本、若干pdb与obj编译中间文件以及tlog、log等构建日志源码与工程结构完整。目前已有5838人学习下载。借助这份源码读者可研究文件I/O操作、二进制数据解析与MFC界面设计的具体实现并在此基础上扩展格式支持或优化转换速度是理解二进制与文本转换细节的实用参考。1. 从一堆乱码到可读文本bin 转 txt 到底在转什么手里拿到一个.bin文件双击打不开用记事本强行打开满屏方块和问号这是很多人第一次接触二进制固件或资源包时的真实场景。bin 文件本质是原始字节流没有统一的内部结构约定它可能是 Keil 编译出的 ARM 固件、FPGA 的比特流配置、游戏贴图资源、字库点阵也可能是某个传感器采集的裸数据。所谓「bin 转 txt」核心工作是把这些字节按某种规则重新解释成人类可读的十六进制、十进制或结构化文本方便比对、检索、二次加工。这个工具面向的是嵌入式调试、固件逆向、数据取证和批量文本处理场景适合需要频繁查看二进制内容又不想每次都开专业十六进制编辑器的从业者。它解决的不是「打开文件」这么简单而是把不可读的字节变成可搜索、可 diff、可脚本化处理的文本资产。2. 转换引擎怎么选三种解析模式与参数含义2.1 十六进制转储模式最通用的兜底方案绝大多数 bin 转 txt 工具默认走的是十六进制转储hex dump路线输出格式类似00000000: 48 65 6C 6C 6F 20 57 6F 72 6C 64 Hello World。这种模式不关心文件内容语义只做字节到字符的映射因此对任何 bin 文件都适用。选它的理由是通用性强、不会因为格式猜测错误而丢数据缺点是输出体积膨胀约 4 到 5 倍一个 1MB 的固件转出来接近 5MB 文本。常见做法是提供三个可调参数每行字节数通常 16 或 32、是否显示 ASCII 侧栏、地址基址从 0 开始还是从芯片 Flash 起始地址开始。地址基址这个参数容易被忽略但在嵌入式场景里很关键——Keil 生成的 bin 文件默认从 0x08000000 开始映射如果转 txt 时基址填 0后续和 map 文件对照时地址全对不上排查起来就是纯玄学。def bin_to_hexdump(src_path, dst_path, bytes_per_line16, base_addr0, show_asciiTrue): with open(src_path, rb) as f: data f.read() lines [] for offset in range(0, len(data), bytes_per_line): chunk data[offset:offset bytes_per_line] # 地址列基址 偏移8 位十六进制 addr f{base_addr offset:08X} # 字节列每个字节两位十六进制不足补空格对齐 hex_part .join(f{b:02X} for b in chunk) hex_part hex_part.ljust(bytes_per_line * 3 - 1) if show_ascii: # ASCII 侧栏可打印字符原样输出其余用点代替 ascii_part .join(chr(b) if 32 b 127 else . for b in chunk) lines.append(f{addr}: {hex_part} {ascii_part}) else: lines.append(f{addr}: {hex_part}) with open(dst_path, w, encodingutf-8) as f: f.write(\n.join(lines))这段代码的逻辑很直白按固定字节数切片每片生成一行「地址 十六进制 ASCII」。bytes_per_line控制每行宽度16 是传统 hexdump 的默认值32 适合宽屏查看base_addr决定地址列的起始值嵌入式场景填0x08000000show_ascii关掉后输出更紧凑适合纯机器解析。实际使用时如果文件超过几十 MB建议改成流式读写避免一次性read()把内存吃满。2.2 结构化解析模式按已知格式提取字段如果 bin 文件来源明确比如是某个传感器的定长记录、某款字库的点阵数据、或者 Keil 编译出的固件就可以跳过通用转储直接按格式定义提取字段。这种模式输出的是「有意义」的 txt比如每行一条记录、每个字段用制表符分隔后续可以直接导入 Excel 或数据库。以定长记录为例假设每条记录 32 字节前 4 字节是时间戳小端序接着 2 字节是温度有符号单位 0.1℃再 2 字节是湿度剩余为保留位。解析脚本需要处理字节序、符号位和单位换算三个坑。import struct def parse_fixed_records(src_path, dst_path, record_size32): with open(src_path, rb) as f: data f.read() records [] for i in range(0, len(data) - record_size 1, record_size): rec data[i:i record_size] # 表示小端序I 无符号 32 位h 有符号 16 位 timestamp, temp_raw, humi_raw struct.unpack(Ihh, rec[:8]) temp temp_raw / 10.0 # 原始值除以 10 得到摄氏度 humi humi_raw / 10.0 records.append(f{timestamp}\t{temp:.1f}\t{humi:.1f}) with open(dst_path, w, encodingutf-8) as f: f.write(timestamp\ttemperature\thumidity\n) f.write(\n.join(records))struct.unpack的格式串Ihh是核心声明小端序I读 4 字节无符号整数当时间戳两个h各读 2 字节有符号整数。温度湿度原始值除以 10 是常见约定具体除数要看传感器手册。record_size必须和实际记录长度一致差一个字节后面全部错位这是结构化解析最典型的翻车点。如果记录之间有帧头帧尾还需要先做同步搜索不能直接按固定步长切。2.3 字符编码转换模式当 bin 里藏的是文本有一类 bin 文件本身就是文本只是编码不是 UTF-8比如老式设备导出的 GBK 日志、UTF-16 的配置字符串、或者带 BOM 的混合编码文件。直接当二进制转十六进制没意义需要先探测编码再转成统一 UTF-8 的 txt。常见做法是用chardet或charset-normalizer做编码探测然后按探测结果解码。但探测不是百分百准短文件尤其容易误判所以工具一般会提供手动指定编码的选项作为兜底。import chardet def bin_to_text(src_path, dst_path, encodingNone): with open(src_path, rb) as f: raw f.read() if encoding is None: # 探测编码confidence 低于 0.7 时回退到 GBK result chardet.detect(raw) encoding result[encoding] if result[confidence] 0.7 else gbk text raw.decode(encoding, errorsreplace) # 无法解码的字节用替换符占位 with open(dst_path, w, encodingutf-8) as f: f.write(text) return encodingerrorsreplace是关键参数遇到非法字节序列时不抛异常而是插入保证转换不中断。代价是原始信息有损如果对完整性要求高应该改成errorssurrogateescape保留原始字节。返回实际使用的编码便于排查——很多时候转出来还是乱码就是因为探测结果和真实编码不一致手动指定gbk或utf-16-le往往能解决。3. 批量转换与命令行封装把工具塞进流水线3.1 目录级批量处理与命名规则单个文件转换用脚本就够了但实际工作中往往是一整个目录的 bin 文件要处理比如 Keil 每次编译输出一个 bin、FPGA 工具链生成一堆比特流、或者日志设备按天导出。这时候需要目录级批量转换并且输出文件的命名要能对应上源文件否则转完一堆 txt 根本分不清谁是谁。常见做法是保持相对路径结构只把扩展名从.bin换成.txt输出到独立的output目录。如果源目录有嵌套用os.walk递归处理并在输出路径里重建同样的层级。import os def batch_convert(src_dir, dst_dir, converter, **kwargs): for root, _, files in os.walk(src_dir): for name in files: if not name.lower().endswith(.bin): continue src_path os.path.join(root, name) # 计算相对路径在输出目录重建相同层级 rel os.path.relpath(src_path, src_dir) dst_path os.path.join(dst_dir, os.path.splitext(rel)[0] .txt) os.makedirs(os.path.dirname(dst_path), exist_okTrue) converter(src_path, dst_path, **kwargs) print(fdone: {rel})os.path.relpath保证输出目录结构和源目录一致os.makedirs(..., exist_okTrue)处理嵌套目录的创建。converter参数传入前面定义的任意转换函数实现模式复用。批量处理时建议加一个跳过已存在文件的判断避免重复转换浪费时间——尤其是大文件重跑一遍可能要好几分钟。3.2 命令行参数设计与退出码脚本给别人用或者塞进 CI 流水线就需要命令行接口。Python 标准库的argparse足够重点是参数命名要直观、默认值要合理、错误时退出码要明确。退出码这个细节很多人忽略但在自动化流程里退出码非零才能让上游脚本感知到失败。import argparse import sys def main(): parser argparse.ArgumentParser(descriptionbin 转 txt 工具) parser.add_argument(input, help输入 bin 文件或目录) parser.add_argument(-o, --output, requiredTrue, help输出 txt 路径或目录) parser.add_argument(-m, --mode, choices[hex, struct, text], defaulthex, help转换模式) parser.add_argument(-b, --base, typelambda x: int(x, 0), default0, help地址基址支持 0x 前缀) parser.add_argument(-w, --width, typeint, default16, help每行字节数) args parser.parse_args() try: if args.mode hex: bin_to_hexdump(args.input, args.output, bytes_per_lineargs.width, base_addrargs.base) elif args.mode struct: parse_fixed_records(args.input, args.output) else: bin_to_text(args.input, args.output) except FileNotFoundError as e: print(f错误文件不存在 {e}, filesys.stderr) sys.exit(2) except Exception as e: print(f转换失败{e}, filesys.stderr) sys.exit(1) if __name__ __main__: main()typelambda x: int(x, 0)让--base同时接受0x08000000和134217728两种写法比强制十六进制友好。退出码约定2 表示输入问题1 表示转换过程出错0 表示成功。sys.stderr输出错误信息避免和正常输出混在一起被重定向吞掉。这套接口封装好后在 Makefile 或 CI 脚本里直接调用即可不需要每次手改代码。3.3 大文件流式处理与内存控制前面几个示例都是f.read()一次性读入文件小的时候没问题但遇到几百 MB 的固件或采集数据就会爆内存。流式处理的核心是分块读取、逐块转换、追加写入内存占用只和块大小有关和文件总大小无关。def stream_hexdump(src_path, dst_path, chunk_size65536, bytes_per_line16, base_addr0): offset 0 with open(src_path, rb) as fin, open(dst_path, w, encodingutf-8) as fout: while True: chunk fin.read(chunk_size) if not chunk: break # 保证块边界对齐到整行避免行被截断 for i in range(0, len(chunk), bytes_per_line): line_bytes chunk[i:i bytes_per_line] addr f{base_addr offset i:08X} hex_part .join(f{b:02X} for b in line_bytes) fout.write(f{addr}: {hex_part}\n) offset len(chunk)chunk_size默认 64KB是内存和 IO 次数的折中。关键点是块边界必须按bytes_per_line对齐处理否则一个 16 字节的行可能被拆到两个块里输出就乱了。offset累加实际读取的字节数保证地址连续。这种写法处理 1GB 文件内存占用也就几十 KB代价是 Python 循环逐行格式化比一次性处理慢一些但对大多数场景够用。4. 避坑与排查那些转出来不对的常见原因4.1 转出来全是问号或方块现象txt 打开后满屏?或□完全不可读。原因通常有两个——一是用文本模式打开了真正的二进制文件编辑器按当前编码强行解释字节二是转换时编码指定错误比如把 GBK 当 UTF-8 解码。解决确认转换模式选的是 hex 而不是 text如果是 text 模式用chardet探测或手动试gbk、utf-16-le、latin-1几个常见编码latin-1能保证任何字节都不报错适合做兜底。4.2 地址对不上 map 文件现象转出的 txt 里地址从 0 开始但 Keil 或 GCC 生成的 map 文件里函数地址都在 0x0800xxxx。原因bin 文件本身不包含地址信息转换时的基址参数默认是 0而实际固件在 Flash 里的起始地址是 0x08000000。解决转换时显式指定--base 0x08000000或者在代码里把base_addr默认值改成芯片的 Flash 起始地址。这个坑在嵌入式调试里非常高频对不上地址时先查基址。4.3 结构化解析字段全部错位现象按定长记录解析第一条数据看着正常后面全乱。原因记录长度估计错误或者记录之间有帧头帧尾、填充字节没算进去。解决先用 hex 模式转一小段人工数一下真实记录占多少字节确认有没有同步头如果记录变长就不能用固定步长切得先做帧同步搜索。另外注意字节序小端序和大端序读出来的数值完全不同struct格式串里的和别写反。4.4 大文件转换中途内存溢出现象转换几百 MB 的文件时进程被系统杀掉或者报MemoryError。原因用了f.read()一次性读入整个文件。解决改用流式分块读取块大小 64KB 到 1MB 之间同时输出也用追加写而不是先拼一个大字符串再写。如果还要做结构化解析确保每条记录的解析不依赖跨块上下文否则需要维护一个滑动缓冲区处理跨块记录。4.5 批量转换后文件名冲突现象不同子目录下的同名 bin 文件转出来后互相覆盖只剩最后一个。原因批量脚本只取了文件名没保留目录结构所有输出都写到同一个目录。解决用os.path.relpath计算相对路径在输出目录重建相同层级或者把相对路径里的分隔符替换成下划线拼进文件名。前者更清晰后者适合输出必须扁平化的场景。5. 进阶技巧把 txt 转换接进自动化验证链路转换本身只是第一步真正省时间的是把 txt 输出接进后续的比对和验证流程。我一般会在转换完成后加一步自动 diff把新转出的 txt 和上一版基线做逐行对比只输出差异行。这样每次固件更新后不用人工翻几千行 hex直接看 diff 结果就知道哪些字节变了。# 转换后自动比对只输出差异 python bin2txt.py firmware_new.bin -o new.txt -m hex -b 0x08000000 diff -u baseline.txt new.txt changes.diff || true # 统计差异行数超过阈值就告警 changed$(grep -c ^[-] changes.diff || true) if [ $changed -gt 100 ]; then echo 警告差异行数 $changed超过阈值 fidiff -u输出统一格式的差异|| true保证 diff 返回非零有差异时不会中断脚本。grep -c ^[-]统计增删行数阈值根据项目实际情况设。这套流程在固件迭代频繁的项目里特别有用能第一时间发现意外改动。另一个技巧是针对特定字段做提取验证。比如只想确认固件版本号有没有变不需要全量 diff直接在 hex 输出里搜版本字符串的 ASCII 侧栏或者用结构化模式只解析版本字段所在偏移。这样输出更聚焦也更快。验证目标推荐模式关键参数输出特征全量字节比对hexbase 对齐芯片起始地址每行地址字节ASCII版本号确认hex 或 text搜索 ASCII 侧栏可 grep 的字符串传感器数据校验structrecord_size 精确匹配制表符分隔的字段编码正确性text手动指定编码无替换符的纯文本从那以后我每次做 bin 转 txt都会先把基址和记录长度这两个参数确认一遍再跑批量跑完立刻做一次 diff 看差异行数是否在预期内。这个习惯帮我挡掉过好几次「以为没改其实改了」的固件事故。希望帮到你。本文还有配套的精品资源点击获取
返回列表