文件结构概述:文件头、文件体、文件尾、魔数、偏移量与对齐
说到二进制文件,很多人第一反应就是「一堆看不懂的十六进制」。其实没那么玄乎。你随便打开一个 .exe、.png 或者 .elf 文件,用十六进制编辑器看一眼,就会发现它是有规律的——不是乱码,而是有结构的。
我个人习惯把二进制文件比作一栋楼。楼有地基、主体和屋顶,文件也有头、体和尾。今天我们就来拆开看看,这栋「数据大楼」到底是怎么盖起来的。
2.1 文件头(File Header)—— 楼的地基
文件头是文件的起点。它告诉解析器三件事:这是什么文件、它有多大、怎么读它。
举个例子,我当年逆向一个嵌入式固件时,发现文件头里藏着芯片型号和固件版本。没有这个头,你根本不知道这段二进制该往哪烧录。
文件头通常包含:
- 魔数(Magic Number)—— 文件类型的身份证
- 版本号 —— 格式迭代的标记
- 文件大小或段大小 —— 方便解析器分配内存
- 偏移量表 —— 指向各个关键结构的指针
核心观点:文件头就是解析器的「使用说明书」。没有它,后面的数据就是一堆废铁。
2.2 魔数(Magic Number)—— 一眼认出你是谁
魔数,说白了就是文件开头的几个固定字节。它让操作系统和工具能瞬间判断文件类型。
常见的魔数长这样:
| 文件类型 | 魔数(十六进制) | ASCII 表示 |
|---|---|---|
| PNG 图片 | 89 50 4E 47 0D 0A 1A 0A |
.PNG...... |
| ELF 可执行文件 | 7F 45 4C 46 |
.ELF |
| JPEG 图片 | FF D8 FF E0 |
...... |
| PDF 文档 | 25 50 44 46 |
%PDF |
| ZIP 压缩包 | 50 4B 03 04 |
PK.. |
你想想看,为什么魔数要放在开头?因为解析器只需要读前几个字节就能做决策,不用把整个文件都加载进来。效率就是这么来的。
小技巧:用 xxd 或 hexdump 看文件头,是逆向的第一步。我习惯先看魔数,再决定用什么工具拆包。
2.3 文件体(File Body)—— 真正的数据仓库
文件体是文件的主体部分。它承载了实际内容——图片的像素、程序的指令、文档的文字。
文件体的结构通常不是一坨连续的字节,而是被划分成多个段(Section)或块(Chunk)。比如:
- ELF 文件:有 .text(代码段)、.data(数据段)、.bss(未初始化数据段)
- PNG 文件:由 IHDR(头块)、IDAT(数据块)、IEND(尾块)组成
- BMP 文件:像素数据按行排列,每行可能还有对齐填充
我记得有一次分析一个自定义的日志文件格式,文件体里每一条记录都带时间戳和长度前缀。解析时只要按长度跳着读就行,跟拆快递一样——每个包裹上都有标签。
2.4 文件尾(File Trailer)—— 收尾的哨兵
文件尾的作用很简单:告诉解析器「到这里就结束了」。
有些格式用固定魔数做结尾,比如 PNG 的 49 45 4E 44 AE 42 60 82(即 IEND 块)。有些格式则用文件大小字段来定位结尾,比如 ELF 文件头里就记录了文件总大小。
注意:文件尾不是可有可无的。我曾经遇到过一个损坏的固件,文件尾被截断了,结果解析器一直读到垃圾数据,直接崩溃。文件尾就是安全绳。
2.5 偏移量(Offset)与对齐(Alignment)—— 精准定位的基石
偏移量,就是某个数据相对于文件开头的字节距离。比如偏移量 0x100 表示从文件开头数 256 个字节的位置。
对齐,则是为了让数据在内存或存储介质上更高效地访问。比如 4 字节对齐,意味着每个数据块的起始地址必须是 4 的倍数。
为什么要有对齐?
- CPU 访问效率:很多 CPU 从对齐地址读数据更快,不对齐可能触发异常
- 硬件限制:某些存储设备(如 NAND Flash)只能按块读写
- 格式规范:比如 BMP 的每行像素数据必须对齐到 4 字节
举个实际例子。我在分析一个老旧的游戏存档文件时,发现它的偏移量表里每个条目都是 8 字节对齐的。一开始我没注意,直接按偏移量读,结果读出来的数据全是乱的。后来才发现,每个条目后面都塞了填充字节(Padding),就是为了对齐。
避坑指南:解析二进制文件时,永远不要假设数据是紧凑排列的。先查对齐规则,再动手写代码。我曾经因为这个吃过亏,浪费了两天时间。
2.6 整体结构图:一张图看懂文件布局
下面这张 SVG 图展示了二进制文件的典型结构。你可以看到文件头、文件体和文件尾是如何衔接的,以及魔数、偏移量、对齐填充在其中的位置。
2.7 实战:如何快速分析一个未知文件的结构
如果你拿到一个陌生的二进制文件,该怎么下手?我一般按这个步骤来:
- 看魔数 —— 用
xxd看前 16 个字节,判断文件类型 - 找偏移量表 —— 通常在文件头里,记录各个段的起始位置
- 计算对齐单位 —— 观察每个段的起始地址是不是 4、8 或 16 的倍数
- 验证文件尾 —— 检查文件末尾是否有固定魔数或长度校验
- 写解析脚本 —— 用 Python 的
struct模块按偏移量读取
我的习惯:先用 hexdump -C 把文件头打印出来,手动标注每个字段。等结构摸清楚了,再写自动化解析代码。别一上来就写脚本,容易翻车。
2.8 本章小结
文件结构其实不复杂。记住三个核心:头告诉你怎么读,体存着真正的内容,尾告诉你读完了。魔数是身份证,偏移量是地图,对齐是规矩。
下次你看到一个二进制文件,别怕。先看头,再找偏移,最后验证尾。这套方法论,我用了十年,从没失手过。