关于二进制文件格式的一点小曲

理解文件格式和转义字符的本质一直是我的一个心结。我最近发现了一些 有用的解释 它们启发我写下自己对二进制文件的理解。

计算机如何表示数据

对计算机而言,一切皆为比特和字节,也就是 1 和 0。人类理解文本,所以我们有程序将一串 1 和 0 转换成我们能理解的东西。

在 ASCII 字符方案中,单个字节(一串八个 1 或 0,或是 0-255 之间的数字)可以转换为一个字符。例如,字符 ‘A’ 在十进制中是数字 65,十六进制中是 41,二进制中是 01000001。‘B’ 在十进制中是数字 66,依此类推(参见一个 完整图表).

不信?来个迷你小例子。

在记事本中创建一个只包含单个字母 “A” 的文件(任意文件名均可 —— “sample.txt”)。

保存文件,右键查看属性 —— 它应该是 1 字节:记事本(Notepad)以 ASCII 存储字符,每个字符占一个字节。“占用空间”可能更大,因为计算机按固定块(例如 4 千字节)分配空间。

notepad_A.png notepad_A_size.png

找一个十六进制编辑器(这里有一个 免费的那个)并打开你刚保存的文件。(在 Linux/Unix 上,使用 “od -x sample.txt”)

你只会看到十六进制中的单个数字“41”(十进制为65),十六进制编辑器可能会在侧边屏幕上显示字符“A”(你所查看字节的 ASCII 表示)。左边的“0”是该字节的地址——程序员喜欢从零开始计数。

notepad_A_hexedit.PNG

十六进制编辑器将所有数据以 ASCII 文本形式显示,在我们的例子中正是如此。如果你打开一个非 ASCII 文件,其中的数据也会以 ASCII 字符显示,尽管可能并不总是有意义。

试着打开一个随机的 .exe 文件,看看里面嵌入了哪些 ASCII 字符串——你通常能在文件开头部分找到一些。所有 DOS 可执行文件都以头部“MZ”开头,即 程序员 提出该文件格式的人的姓名首字母。

hexedit_notepad.PNG

很酷吧?这些头部或“魔数(magic numbers)”是程序判断所看到文件类型的一种方式。如果你打开一张 PNG 图片,你会看到 PNG 头部,其中包含 ASCII 字母“PNG”

发生了什么?

在计算机内存中,sample.txt 里只存储了“65”(十六进制为41,二进制为01000001)。鉴于信息的上下文(即记事本(Notepad)预期这是一个文本文件),计算机知道在屏幕上显示 ASCII 字符‘A’。

现在想想,如果你让人把实际的 数值 65 写下来,人类会怎么存储它。作为人类,我们会把它写成两个字符,先是‘6’然后是‘5’,这需要 2 个 ASCII 字符或 2 个字节(同样,“数字”6 也可以用 ASCII 存储)。

计算机会把数字“65”以二进制形式的 65 存储,和‘a’一样。只不过这一次,软件会知道这个‘65’不是某个字母的编码,而是数字本身。

现在,假设我们想存储数字 4,000,000,000(40亿)。作为人类,我们会把它写成 4000000000,也就是 10 个 ASCII 字符(10 个字节)。计算机会怎么做呢?

一个字节有 8 位,即 2^8(256)种可能的值。4 个字节给我们 2^32 位,即 大约 40 亿个值。所以,我们只需 4 个字节就能存储 40 亿这个数字。

如你所见,以计算机的格式存储数值数据可以节省空间。它还节省了计算开销——计算机无需在二进制和 ASCII 之间转换数字。

那么,为什么不使用二进制格式?

如果二进制格式更高效,为什么不一直使用它们呢?

  • 二进制文件对人类来说难以阅读。 当一个人看到 4 个字节的序列时,他不知道这意味着什么(它可能是一个以 ASCII 存储的 4 字母单词)。如果他看到 10 个 ASCII 字母 4000000000,他就知道这是一个数字。
  • 二进制文件难以编辑。 同样地,如果一个人想把 40 亿改成 20 亿,他需要知道二进制表示。而使用 ASCII 表示,他只需把“4”换成“2”即可。
  • 二进制文件难以操作。 UNIX 传统中有若干简单优雅的工具来操作文本。通过将文件存储为标准文本格式,你无需创建专门的编辑器来修改二进制文件,就能获得这些工具的能力。
  • 二进制文件可能会令人困惑。 当计算机读取数据的方式不同时,问题就出现了。有一种叫做“NUXI”或 字节序问题的问题,它发生在架构不同的两台计算机(例如 PowerPC Mac 和 x86 PC)尝试传输二进制数据时。以单字节存储的常规文本是无歧义的,但使用 unicode(统一码).
  • 效率提升通常并不显著。 用二进制表示数字理想情况下可节省 3 倍空间(一个 4 字节数字可表示 10 字节文本)。但这假设你所表示的数字很大(像 999 这样的 3 位数用 ASCII 表示比用 4 字节数字更好)。最后,ASCII 实际上每字节只用 7 位,所以理论上你可以把 ASCII 紧凑打包获得 1/8 即 12% 的提升。然而,以这种方式存储文本通常得不偿失。

二进制文件高效的原因之一,是它们能用上一个字节中的所有 8 位,而大多数文本受限于某些固定模式,留下了未使用的空间。不过,通过 压缩你的文本数据 你可以减少所占空间,让文本更高效。

数据的编组(Marshalling)与解组(Unmarshalling)

题外话:Marshalling 总是让我想到治安官(Sheriff Marshal),进而想到牛仔。牛仔和“marshal”在计算机科学中的含义毫无关系。

有时计算机拥有复杂的内部数据结构,带有需要存入文件的链式关联项。 Marshalling(编组) 序列化(Serialization) 是将程序的内部数据取出并保存到扁平、线性文件中的过程。 Unmarshalling(解组) 反序列化(Deserialization) 是读取那些线性数据并重建计算机原本拥有的复杂内部数据结构的过程。

记事本(Notepad)很轻松——它只需存储原始文本,因此不需要编组(Marshalling)。然而,Microsoft Word 必须将文本连同其他文档信息(页边距、字体大小、嵌入图像、样式等)一起存储在单个线性文件中。之后,它必须读取该文件并重建用户原有的设置。

你可以将数据编组(Marshalling)为二进制或文本格式——“编组(Marshalling)”一词并不表示数据是如何存储的。

那么二进制文件格式什么时候有用?

有些情况下你可能想使用二进制文件格式。PNG 图像使用二进制格式,因为在创建小型图像文件时效率很重要。不过,PNG 的二进制格式做得很好:它指定了字节序和字长,以避免 NUXI 问题。

使用二进制格式往往有商业上的原因。主要原因是它们更难被逆向工程(人类必须猜测计算机如何存储其数据),这有助于保持竞争优势。

本系列其他文章

  1. 数制(Number Systems)与进制(Bases)
  2. GUID 快速指南
  3. 理解 Quake 的快速平方根倒数(Fast Inverse Square Root)算法
  4. 计算机网络简明入门
  5. 使用异或(XOR)交换两个变量
  6. 理解大端(Big Endian)与小端(Little Endian)字节序
  7. Unicode 与你
  8. 关于二进制文件格式的一点小曲
  9. 排序算法(Sorting Algorithms)

加入 45 万月度读者

喜欢这篇文章?还有更多内容能帮你建立持久、直观的数学理解。加入通讯以获取额外内容和最新更新。