二进制文件“二进制”文件和“文本”文件之间的区别
本文探讨了“二进制”文件和“文本”文件。这两者之间有什么区别(如果有的话)?对于“二进制”文件和“文本”文件的构成,有明确的定义吗?
我们从两个候选文件开始我们的旅程,我们直观地将其内容分别归类为“文本”和“二进制”数据:
bash
echo "hello 🌍" > message
convert -size 1x1 xc:white png:white
我们创建了两个文件:一个名为“hello 🌍”的文件message
(包含Unicode 符号“Earth Globe Europe-Africa”),以及一个名为 的包含单个白色像素的 PNG 图像white
。文件扩展名被故意省略了。
为了证明某些程序区分“文本”和“二进制”文件,请查看grep
其行为如何改变:
▶ grep -R hello
message:hello 🌍
▶ grep -R PNG
Binary file white matches
diff
做类似的事情:
▶ echo "hello world" > other-message
▶ diff other-message message
1c1
< hello world
---
> hello 🌍
▶ convert -size 1x1 xc:black png:black
▶ diff black white
Binary files black and white differ
这些程序如何区分“文本”和“二进制”文件?
在回答这个问题之前,我们先来尝试给出一个定义。显然,在文件系统的基本层面上,每个文件都只是字节的集合,因此可以被视为二进制数据。另一方面,区分“文本”和“非文本”(以下简称“二进制”)数据似乎对像grep
或 这样的程序很有帮助diff
,哪怕只是为了不弄乱终端仿真器的输出。
因此,我们可以先定义“文本”数据。首先将文本抽象为一系列Unicode 代码点,这似乎很合理。代码点的示例包括字符(例如k
、ä
或א
),以及特殊符号(例如%
、☢
或)🙈
。要将给定的文本存储为字节序列,我们需要选择一种编码。如果我们想要表示整个 Unicode 范围,通常会选择 UTF-8,有时也会选择 UTF-16 或 UTF-32。从历史上看,仅支持当今部分 Unicode 的编码也很重要。最著名的是 US-ASCII 和 Latin1(ISO 8859-1),但还有更多。所有这些编码在字节级别上看起来都不同。
因此,仅给出文件的内容(而不是其创建历史),我们可以尝试以下定义:
如果文件的内容由 Unicode 代码点的编码序列组成,则该文件被称为“文本文件”。
这个定义有两个实际问题。首先,我们需要一个包含所有可能编码的列表。其次,为了测试文件内容是否以给定的编码进行编码,我们必须解码整个文件内容并查看是否成功¹。整个过程会非常缓慢。
事实证明,有一种更快的方法来区分文本和二进制文件,但这是以精度为代价的。
为了了解其工作原理,让我们回到两个候选文件并探索它们的字节级内容。我使用的hexyl
是十六进制查看器,但您也可以使用hexdump -C
:
请注意,这两个文件都包含 ASCII 范围(00
… )内和范围外的字节。例如,文件中的7f
四个字节是 Unicode 码位(🌍)的 UTF-8 编码版本。另一方面,图像开头的字节是字符 ² 的简单 ASCII 编码版本。f0 9f 8c 8d
message
U+1F30D
50 4e 47
white
PNG
显然,查看 ASCII 范围之外的字节不能用作检测“二进制”文件的方法。然而,这两个文件之间存在差异。图像文件包含大量 NULL 字节(00
),而短文本消息则没有。事实证明,这可以转化为一种简单的启发式方法来检测二进制文件,因为许多编码的文本数据不包含任何 NULL 字节(即使它可能是合法的)。
事实上,这正是diff
和用来检测“二进制”文件的方法。s源代码grep
中包含以下宏( ):diff
src/io.c
#define binary_file_p(buf, size) (memchr (buf, 0, size) != 0)
这里,memchr(const void *s, int c, size_t n)
函数用于size
在从 开始的内存区域的初始字节中搜索buf
字符0
。为了进一步加快此过程,通常只将文件的前几个字节buf
(例如 1024 个字节)读入缓冲区。总结一下,grep
并diff
使用以下启发式方法:
如果文件内容的前 1024 个字节不包含任何 NULL 字节,则文件很可能是“文本文件”。
请注意,存在一些反例,表明此方法会失效。例如,即使可能性不大,UTF-8 编码的文本可以合法地包含 NULL 字节。相反,某些特定的二进制格式(例如二进制PGM)不包含 NULL 字节。此方法通常也会将 UTF-16 和 UTF-32 编码的文本归类为“二进制”,因为它们使用 NULL 字节编码常见的 Latin-1 码位:
▶ iconv -f UTF-8 -t UTF-16 message > message-utf16
▶ hexdump -C message-utf16
00000000 ff fe 68 00 65 00 6c 00 6c 00 6f 00 20 00 3c d8 |..h.e.l.l.o. .<.|
00000010 0d df 0a 00 |....|
00000014
▶ grep . message-utf16
Binary file message-utf16 matches
尽管如此,这种启发式方法非常有用。我用 Rust 编写了一个小型库,它使用了这个方法的稍微改进版本,可以快速判断给定文件包含的是“二进制”数据还是“文本”数据。在我的程序中,它被用来bat
防止“二进制”文件被转储到终端:
脚注
¹ 请注意,有些编码会在文件开头写入所谓的字节顺序标记ff fe 00 00
(BOM),以指示编码类型。例如,UTF-32 的小端字节序变体使用。这些 BOM 有助于解决第二点问题,因为我们无需解码文件的全部内容。遗憾的是,添加 BOM 是可选的,很多编码并没有指定 BOM。
²是 PNG 格式的魔数 (magic number)50 4e 47
的一部分。魔数类似于 BOM,许多二进制格式在文件开头使用魔数来指示其类型。该工具使用魔数来检测某些类型的“二进制”文件。file