逆向一种私有二进制格式从文件头和 magic 字节说起
手上有一批设备采集下来的二进制数据,没文档,只有几组不同后缀的文件,要把它们读出来画图。全程靠 hex 编辑器一点点猜,记一下逆向这种私有二进制格式的大概路数,顺便记一个踩得很疼的 off-by-one。
先把头和样点数摸清楚
这种采集文件,基本结构都是「一个文件头 + 后面一大片采样数据」。第一步是估文件头多大。把文件丢进 hex 编辑器,前面一段往往是固定模式、跟后面的「数据区」长得不一样,头一般是个整数(512、1024、2048 这种)。这批文件摸下来头是 1024 字节。
样点数通常藏在文件头的某个偏移里。怎么找——算一道简单除法:(文件总大小 - 头) / 每道字节数 = 道数,而 每道字节数 = 样点数 × 每个样点的字节。如果数据是 16 位(2 字节)存的,那去文件头里找一个值,使得「头之后的字节数 / 2 / 这个值」是个整数、而且这个值本身是合理的样点数(比如 512)。这批文件样点数在头偏移 4 的位置,值是 512。
到这里,最朴素的读法就有了:跳过 1024 头,后面按 int16 一道一道读:
nsamp = 512 # 头里偏移4读出来的样点数 |
用 magic 字节区分格式
事情没这么简单,因为这批数据有四种后缀、对应四种格式,得知道手里这份是哪种。靠后缀判断不可靠(后缀能随便改),靠谱的是文件头里的「魔数」——每个格式在固定位置有一段独特的字节,相当于这个格式的身份证。
实测下来这批文件:一种开头是 00 07,一种是 56 04 00 00 04 04 00 00,另两种是 01 00 或 00 00。读的时候先嗅探(sniff)一下这几个字节,命中哪种就走哪种的解析器,后缀只当兜底:
def read_auto(path): |
有意思的是,其中三种格式摸到最后发现是同一个套路——都是 [1024 头][int16 数据],只是 magic 不一样,连样点数偏移都一样(纯属不同厂家巧合撞了布局)。所以这三个的解析逻辑抽到了一个公共函数,各自只是薄薄包一层填自己的 magic。
一个 off-by-one,数据道数多算一条
真正折腾人的是第四种。一开始按老套路把它当「[1024 头][int16]」来读,画出来的图第一道总是一根奇怪的直线、后面整体对不齐,怎么调参数都不对。
后来把它的字节流摊开一格一格看,才发现它根本不是「全局头 + 数据」那种结构,而是定长记录流:整个文件就是一条条固定 1028 字节的记录首尾拼接,没有独立的全局头。而且记录还分类型:
- 第 0 条是「头记录」,前 4 字节是类型码
1110,后面基本是 0; - 后面才是数据道记录,每条开头是类型码
82,再跟 512 个 int16 样点。
之前的读法把第 0 条头记录也当成了数据道,所以道数多算了一条、第一道里混进了那条头记录的残值,图自然对不齐。改完之后,先检测开头是不是 1110、是的话跳过这条头记录,真实道数从第 1 条开始数,图一下就正了。
def read_record_stream(data): |
逆向这种没文档的二进制格式,路数其实挺固定:先估头大小、再从文件大小反推样点数偏移、用 magic 区分同源的几种格式、剩下就是格格里看字节把记录结构猜出来。最后那个 off-by-one 也提醒一件事——别想当然觉得同一批数据都是一种结构,哪怕是同一个采集软件出的,不同后缀可能用的是完全不同的文件模型,按老套路硬套就会多算一条少算一条。
