Hexter

深入二进制文件格式:详解 PNG、ZIP 与 Mach-O 头部

你每天都在用的每一种文件格式,无论是图像、归档还是可执行文件,都有一套精确到字节的结构,而这些结构几乎没有一点能通过你平常用来打开这些文件的工具看到。预览显示给你一张图片。归档实用工具显示给你解压后的文件。两者都不会显示造就这些结果的那些字节。下面就来看看三种常见格式底下究竟长什么样,以及你如何亲自去看它们。

PNG:一连串带标签的分块

每一个 PNG 文件都以相同的八个字节开头:89 50 4E 47 0D 0A 1A 0A。那不是图像数据,而是一个签名,特意选择了包含在文件被误当作文本传输时会变得无法使用的字节(0x0A 和 0x0D 是换行字符,所以一次出错的文本模式传输会以一种容易被检测到的方式破坏它们)。

在签名之后,PNG 里的一切都是分块,而每个分块都遵循同样的四部分布局:

PNG 分块结构示意图:一个 4 字节的长度字段、一个像 IHDR 或 IDAT 那样的 4 字节类型字段、分块数据本身,以及末尾一个 4 字节的 CRC 校验和。
每一个 PNG 分块,从第一个(IHDR)到最后一个(IEND),都遵循这同样的四字段布局。
  • 长度(4 字节):后面跟着多少字节的数据,不计入类型或 CRC。
  • 类型(4 字节,始终为 ASCII):一个四字母的代码,比如 IHDR(图像头,始终排在最前)、IDAT(压缩后的像素数据,可以有多个)或 IEND(标记文件结束)。
  • 数据:由该分块类型所规定的内容。IHDR 的数据是宽度、高度、位深度和颜色类型,始终是同样的固定布局。
  • CRC(4 字节):对类型和数据的校验和,好让解码器能检测出损坏的分块,而不是悄悄地误读它。

这就是整个格式。一个 PNG 解码器只是一个接一个地走过每个分块,读取长度来知道该跳多远,直到碰上 IEND

ZIP:先是本地文件头,末尾再来一个目录

一个 ZIP 归档为每个文件都存储了它自己的本地文件头,以签名 50 4B 03 04 标记(开头的 PK 是对该格式创造者 Phil Katz 的致意)。这个文件头包含文件名、压缩方法,以及压缩前和压缩后两个大小,紧接着就是该文件的压缩数据。

让人栽跟头的地方在于:本地文件头里那些每个文件的大小和校验和有时是零,真正的数值稍后存储在一个"数据描述符"里,因为 ZIP 在设计时就要支持流式写入,也就是在数据已经写完之前并不知道最终大小。关于归档里究竟有什么的权威清单,存在于文件最末尾的一个中央目录里,这也是为什么有些损坏或被截断的 ZIP,在你从头扫描时看起来像是有内容,却打不开,因为你用的工具会先读取中央目录,而它缺失或损坏了。

Mach-O:先是魔数,然后是加载命令

Mach-O 是 macOS 用于编译后二进制的可执行格式。它以一个魔数开头,这个魔数一次告诉加载器两件事:它是 32 位还是 64 位,以及文件其余部分使用哪种字节序。0xFEEDFACE 是 32 位,0xFEEDFACF 是 64 位,而如果你看到字节被反转(0xCEFAEDFE0xCFFAEDFE),那意味着这个文件是以与读取它的机器相反的字节序写入的。

魔数之后是一个固定的头部(CPU 类型、文件类型,以及加载命令的数量),后面跟着那么多个依次排列的加载命令。加载命令是 Mach-O 文件描述加载器所需一切的方式:要把哪些段映射进内存、要链接哪些动态库、入口点在哪里。这和 PNG 的分块是类似的思路:一个固定的头部,然后是一连串自描述的记录,每一条都告诉读取者要跳过多少才能到达下一条。

亲自看看这些

这一切都不需要写解析器。在 Hexter 中打开任何 PNG、ZIP 或 Mach-O 文件,分析标签页就会自动识别格式,走过真实的分块或加载命令结构,并把它以一棵树的形式陈列在原始十六进制旁边,让你能点击一个字段,看到它究竟来自哪些字节。它默认只读,所以在一个真实的文件上这么做是确实安全的,无论是一个应用二进制、一张截图,还是一个你下载来的归档,都不会有改动它的风险。

如果你想改为亲手验证上面所说的内容:打开一个 PNG,检查前八个字节是否与签名相符,然后看第 8 到第 11 个字节(第一个分块的长度)和第 12 到第 15 个字节(它的类型,应该读作 IHDR)。这是一个足够小、几分钟就能做完的练习,也是一种确实有用的方式,去理解一个你用了一辈子却从未真正看过的文件格式。