文件如何包含空字节?
How can a file contain null bytes?
在用带有空终止字符串的语言(即 C)编写的操作系统中,文件怎么可能包含空字节?
例如,如果我 运行 这个 shell 代码:
$ printf "Hello[=10=], World!" > test.txt
$ xxd test.txt
0000000: 4865 6c6c 6f00 2c20 576f 726c 6421 Hello., World!
我在 test.txt
中看到一个空字节(至少在 OS X 中)。如果 C 使用空终止字符串,并且 OS X 是用 C 编写的,那么为什么文件没有在空字节处终止,导致文件包含 Hello
而不是 Hello[=13= ], World!
?文件和字符串之间有根本区别吗?
Null-terminated 字符串是一种 C 结构,用于确定打算用作字符串的字符序列的结尾。 strcmp
、strcpy
、strchr
等字符串操作函数和其他函数使用此构造来执行其职责。
但是您仍然可以在程序中以及从文件中读取和写入包含空字节的二进制数据。您不能将它们视为字符串。
这是一个如何工作的例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp = fopen("out1","w");
if (fp == NULL) {
perror("fopen failed");
exit(1);
}
int a1[] = { 0x12345678, 0x33220011, 0x0, 0x445566 };
char a2[] = { 0x22, 0x33, 0x0, 0x66 };
char a3[] = "Hello\x0World";
// this writes the whole array
fwrite(a1, sizeof(a1[0]), 4, fp);
// so does this
fwrite(a2, sizeof(a2[0]), 4, fp);
// this does not write the whole array -- only "Hello" is written
fprintf(fp, "%s\n", a3);
// but this does
fwrite(a3, sizeof(a3[0]), 12, fp);
fclose(fp);
return 0;
}
out1 的内容:
[dbush@db-centos tmp]$ xxd out1
0000000: 7856 3412 1100 2233 0000 0000 6655 4400 xV4..."3....fUD.
0000010: 2233 0066 4865 6c6c 6f0a 4865 6c6c 6f00 "3.fHello.Hello.
0000020: 576f 726c 6400 World.
对于第一个数组,因为我们使用fwrite
函数并告诉它写入4个int
大小的元素,所以数组中的所有值都会出现在文件中。从输出可以看出所有的值都被写入了,值都是32位的,每个值都是按照little-endian字节顺序写入的。我们还可以看到,数组的第二个和第四个元素各包含一个空字节,而第三个值为 0 的元素有 4 个空字节,并且都出现在文件中。
我们还在第二个数组上使用 fwrite
,它包含 char
类型的元素,我们再次看到所有数组元素都出现在文件中。特别是,数组中的第三个值是 0,它由一个也出现在文件中的空字节组成。
第三个数组首先使用 fprintf
函数使用 %s
格式说明符写入,该格式说明符需要一个字符串。它在遇到空字节之前将该数组的前 5 个字节写入文件,然后停止读取该数组。然后它根据格式打印换行符 (0x0a
)。
它再次写入文件的第三个数组,这次使用fwrite
。字符串常量 "Hello\x0World"
包含 12 个字节:5 个用于 "Hello",1 个用于显式空字节,5 个用于 "World",1 个用于隐式结束字符串常量的空字节。由于 fwrite
给出了数组的完整大小 (12),因此它会写入所有这些字节。事实上,查看文件内容,我们看到了每个字节。
附带说明一下,在每个 fwrite
调用中,我都对第三个参数的数组大小进行了硬编码,而不是使用 sizeof(a1)/sizeof(a1[0])
等更动态的表达式来进行更清楚每种情况下到底写入了多少字节。
Null-terminated 字符串当然 不是 唯一可以放入文件的东西。操作系统代码不将文件视为存储 null-terminated 字符串的载体:操作系统将文件呈现为任意字节的集合。
就 C 而言,I/O 存在用于以二进制模式写入文件的 API。这是一个例子:
char buffer[] = {0, 1, 0, 2, 0, 3, 0, 4, 0, 5};
FILE *f = fopen("data.bin","wb"); // "w" is for write, "b" is for binary
fwrite(buffer, 1, sizeof(buffer), f);
此 C 代码创建一个名为 "data.bin" 的文件,并向其中写入十个字节。请注意,虽然 buffer
是一个字符数组,但它 不是 一个 null-terminated 字符串。
虽然 null-bytes 用于终止字符串并且需要字符串操作函数(因此它们知道字符串在哪里结束),但在二进制文件中 [=10=]
字节无处不在。
例如,考虑一个包含 32 位数字的二进制文件,如果它们的值小于 2^24,它们将全部包含 null-bytes(例如:0x001a00c7,或 64 位 0x0000000a00001a4d).
与 Unicode-16 相同,其中所有 ASCII 字符都有前导或尾随 [=10=]
,具体取决于它们的 endianness,并且字符串需要以 [=12=][=12=]
.[=17= 结尾]
许多文件甚至用 [=10=]
字节填充块(到 4kB 甚至 64kB),以便快速访问所需的块。
要在文件中包含更多 null-bytes,请查看 sparse files,默认情况下所有字节都是 [=10=]
,充满 null-bytes 的块不是'甚至存储在磁盘上以保存 space。
因为文件只是字节流,任意字节,包括空字节。有些文件仅包含所有可能字节的子集时称为文本文件:可打印的字节(大致为字母数字、空格、标点符号)。
C 字符串是由空字节终止的字节序列,这只是一个约定问题。它们常常是混乱的根源;只是一个以 null 结尾的序列,意味着任何以 null 结尾的 non-null 字节都是正确的 C 字符串!即使是包含不可打印字节或控制字符的。要小心,因为你的例子不是 C 的!在 C 中,printf("dummy[=10=]0foo");
永远不会打印 foo
,因为 printf
将考虑从 d
开始并在中间的空字节处结束的 C 字符串。一些编译器抱怨这样的 C 字符串文字。
现在 C 字符串(通常也只包含可打印字符)和文本文件之间没有直接 link。虽然将 C 字符串打印到文件中通常只存储其非空字节的子序列。
在回答任何问题之前,请注意
(注意: 根据 n.m。(见 OP 中的评论)"a Byte is the smallest quantity available to write out to disk with the C standard library, non-standard libraries may well deal with bits or anything else." 所以我在下面所说的 WORD 大小是最小的数量是可能不是很正确,但仍然提供洞察力)。
NULL 总是 0_decimal(实际上)
dec: 0
hex: 0x00000000
bin: 00000000 00000000 00000000 00000000
虽然它的实际值是由编程语言的规范定义的,所以使用定义的常量 NULL
而不是在任何地方硬编码 0
(以防它发生变化,当地狱冻结时)。
ASCII 字符 '0' 的编码是 48_decimal
dec: 48
hex: 0x00000030
bin: 00000000 00000000 00000000 00110000
NULL
的概念不存在于文件中,而是存在于生成应用程序的编程语言中。文件中只存在 NULL
的数字 encoding/value。
How is it possible that files can contain null bytes in operating
systems written in a language with null-terminating strings (namely,
C)?
有了上面的陈述,这个问题就变成了,一个文件怎么能包含 0? 现在答案很简单了。
For example, if I run this shell code:
$ printf "Hello[=12=], World!"
test.txt $ xxd test.txt 0000000: 4865
6c6c 6f00 2c20 576f 726c 6421 Hello., World!
I see a null byte in test.txt (at least in OS X). If C uses
null-terminating strings, and OS X is written in C, then how come the
file isn't terminated at the null byte, resulting in the file
containing Hello
instead of Hello[=21=], World!
?
Is there a fundamental difference between files and strings?
假设一个ASCII字符编码(1-byte/8-bit个字符在0和127的十进制范围内):
- 字符串 是 buffers/char-arrays 个 1 字节字符(其中 NULL = 0_decimal 和 '0' = 48_decimal))。
- 文件 是 32 位或 64 位序列“WORDS”(取决于 OS 和硬件,即分别为 x86 或 x64)。
因此,一个仅包含 ASCII 字符串的 32 位 OS 文件将是一个 32 位(4 字节)字序列该范围在十进制值 0 和 127 之间,基本上只使用 4 字节字的第一个字节(b2:base-2,十进制是 base-10 和十六进制 base-16,fyi)
0_b2: 00000000 00000000 00000000 00000000
32_b2: 00000000 00000000 00000000 00100000
64_b2: 00000000 00000000 00000000 01000000
96_b2: 00000000 00000000 00000000 01100000
127_b2: 00000000 00000000 00000000 11111111
128_b2: 00000000 00000000 00000001 00000000
这个字节是 left-most 还是 right-most 取决于 OS 的 endianness.
但是为了回答你关于 Hello[=21=], World!
之后缺少的 NULL
的问题,我假设它被 EOL/EOF[=91= 取代了](文件结尾)值,这很可能是 non-printable,这就是为什么您在输出中看不到它的原因 window.
注意: 我确定现代 OS(和经典的基于 Unix 的系统)优化了 ASCII 的存储个字符,这样1个字(4个字节)可以打包成4个字符。 UTF 但是,由于这些编码使用更多位来存储字符,因此它们有更大的 alphabets/character 集来表示(例如50k Kanji/Japanese 个字符)。我认为 UTF-8 类似于 ASCII,为了统一而重命名(与UTF-16 和 UTF-32).
注意: C/C++ 实际上使用字符数组(即字符串)将 "pack" 4 个字符放入一个 4 字节的单词中。由于每个 char 都是 1 字节,因此编译器将在堆栈或堆上分配并在算术上将其视为 1 字节。所以如果你在一个函数中声明一个数组(即一个auto-variable),像这样
char[] str1[7] = {'H','e','l','l','o','!','[=14=]'};
函数堆栈从地址 1000_b10 (base-10/decimal) 开始,然后你有:
072 101 108 108 111 033
addr char binary decimal
---- ----------- -------- -------
1000: str1[0] 'H' 01001000 (072)
1001: str1[1] 'e' 01100101 (101)
1002: str1[2] 'l' 01101100 (108)
1003: str1[3] 'l' 01101100 (108)
1004: str1[4] 'o' 01101111 (111)
1005: str1[5] '!' 00100001 (033)
1006: str1[6] '0' 00000000 (000)
由于 RAM 是 byte-addressable,每个地址引用一个字节。
考虑将数据写入文件的常用 C 函数调用 — write(2)
:
ssize_t
write(int fildes, const void *buf, size_t nbyte);
…和fwrite(3)
:
size_t
fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
这些函数都不接受 const char *
NUL-terminated 字符串。相反,它们采用具有明确大小的字节数组(const void *
)。这些函数像处理任何其他字节值一样处理 NUL 字节。
在用带有空终止字符串的语言(即 C)编写的操作系统中,文件怎么可能包含空字节?
例如,如果我 运行 这个 shell 代码:
$ printf "Hello[=10=], World!" > test.txt
$ xxd test.txt
0000000: 4865 6c6c 6f00 2c20 576f 726c 6421 Hello., World!
我在 test.txt
中看到一个空字节(至少在 OS X 中)。如果 C 使用空终止字符串,并且 OS X 是用 C 编写的,那么为什么文件没有在空字节处终止,导致文件包含 Hello
而不是 Hello[=13= ], World!
?文件和字符串之间有根本区别吗?
Null-terminated 字符串是一种 C 结构,用于确定打算用作字符串的字符序列的结尾。 strcmp
、strcpy
、strchr
等字符串操作函数和其他函数使用此构造来执行其职责。
但是您仍然可以在程序中以及从文件中读取和写入包含空字节的二进制数据。您不能将它们视为字符串。
这是一个如何工作的例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp = fopen("out1","w");
if (fp == NULL) {
perror("fopen failed");
exit(1);
}
int a1[] = { 0x12345678, 0x33220011, 0x0, 0x445566 };
char a2[] = { 0x22, 0x33, 0x0, 0x66 };
char a3[] = "Hello\x0World";
// this writes the whole array
fwrite(a1, sizeof(a1[0]), 4, fp);
// so does this
fwrite(a2, sizeof(a2[0]), 4, fp);
// this does not write the whole array -- only "Hello" is written
fprintf(fp, "%s\n", a3);
// but this does
fwrite(a3, sizeof(a3[0]), 12, fp);
fclose(fp);
return 0;
}
out1 的内容:
[dbush@db-centos tmp]$ xxd out1
0000000: 7856 3412 1100 2233 0000 0000 6655 4400 xV4..."3....fUD.
0000010: 2233 0066 4865 6c6c 6f0a 4865 6c6c 6f00 "3.fHello.Hello.
0000020: 576f 726c 6400 World.
对于第一个数组,因为我们使用fwrite
函数并告诉它写入4个int
大小的元素,所以数组中的所有值都会出现在文件中。从输出可以看出所有的值都被写入了,值都是32位的,每个值都是按照little-endian字节顺序写入的。我们还可以看到,数组的第二个和第四个元素各包含一个空字节,而第三个值为 0 的元素有 4 个空字节,并且都出现在文件中。
我们还在第二个数组上使用 fwrite
,它包含 char
类型的元素,我们再次看到所有数组元素都出现在文件中。特别是,数组中的第三个值是 0,它由一个也出现在文件中的空字节组成。
第三个数组首先使用 fprintf
函数使用 %s
格式说明符写入,该格式说明符需要一个字符串。它在遇到空字节之前将该数组的前 5 个字节写入文件,然后停止读取该数组。然后它根据格式打印换行符 (0x0a
)。
它再次写入文件的第三个数组,这次使用fwrite
。字符串常量 "Hello\x0World"
包含 12 个字节:5 个用于 "Hello",1 个用于显式空字节,5 个用于 "World",1 个用于隐式结束字符串常量的空字节。由于 fwrite
给出了数组的完整大小 (12),因此它会写入所有这些字节。事实上,查看文件内容,我们看到了每个字节。
附带说明一下,在每个 fwrite
调用中,我都对第三个参数的数组大小进行了硬编码,而不是使用 sizeof(a1)/sizeof(a1[0])
等更动态的表达式来进行更清楚每种情况下到底写入了多少字节。
Null-terminated 字符串当然 不是 唯一可以放入文件的东西。操作系统代码不将文件视为存储 null-terminated 字符串的载体:操作系统将文件呈现为任意字节的集合。
就 C 而言,I/O 存在用于以二进制模式写入文件的 API。这是一个例子:
char buffer[] = {0, 1, 0, 2, 0, 3, 0, 4, 0, 5};
FILE *f = fopen("data.bin","wb"); // "w" is for write, "b" is for binary
fwrite(buffer, 1, sizeof(buffer), f);
此 C 代码创建一个名为 "data.bin" 的文件,并向其中写入十个字节。请注意,虽然 buffer
是一个字符数组,但它 不是 一个 null-terminated 字符串。
虽然 null-bytes 用于终止字符串并且需要字符串操作函数(因此它们知道字符串在哪里结束),但在二进制文件中 [=10=]
字节无处不在。
例如,考虑一个包含 32 位数字的二进制文件,如果它们的值小于 2^24,它们将全部包含 null-bytes(例如:0x001a00c7,或 64 位 0x0000000a00001a4d).
与 Unicode-16 相同,其中所有 ASCII 字符都有前导或尾随 [=10=]
,具体取决于它们的 endianness,并且字符串需要以 [=12=][=12=]
.[=17= 结尾]
许多文件甚至用 [=10=]
字节填充块(到 4kB 甚至 64kB),以便快速访问所需的块。
要在文件中包含更多 null-bytes,请查看 sparse files,默认情况下所有字节都是 [=10=]
,充满 null-bytes 的块不是'甚至存储在磁盘上以保存 space。
因为文件只是字节流,任意字节,包括空字节。有些文件仅包含所有可能字节的子集时称为文本文件:可打印的字节(大致为字母数字、空格、标点符号)。
C 字符串是由空字节终止的字节序列,这只是一个约定问题。它们常常是混乱的根源;只是一个以 null 结尾的序列,意味着任何以 null 结尾的 non-null 字节都是正确的 C 字符串!即使是包含不可打印字节或控制字符的。要小心,因为你的例子不是 C 的!在 C 中,printf("dummy[=10=]0foo");
永远不会打印 foo
,因为 printf
将考虑从 d
开始并在中间的空字节处结束的 C 字符串。一些编译器抱怨这样的 C 字符串文字。
现在 C 字符串(通常也只包含可打印字符)和文本文件之间没有直接 link。虽然将 C 字符串打印到文件中通常只存储其非空字节的子序列。
在回答任何问题之前,请注意
(注意: 根据 n.m。(见 OP 中的评论)"a Byte is the smallest quantity available to write out to disk with the C standard library, non-standard libraries may well deal with bits or anything else." 所以我在下面所说的 WORD 大小是最小的数量是可能不是很正确,但仍然提供洞察力)。
NULL 总是 0_decimal(实际上)
dec: 0
hex: 0x00000000
bin: 00000000 00000000 00000000 00000000
虽然它的实际值是由编程语言的规范定义的,所以使用定义的常量 NULL
而不是在任何地方硬编码 0
(以防它发生变化,当地狱冻结时)。
ASCII 字符 '0' 的编码是 48_decimal
dec: 48
hex: 0x00000030
bin: 00000000 00000000 00000000 00110000
NULL
的概念不存在于文件中,而是存在于生成应用程序的编程语言中。文件中只存在 NULL
的数字 encoding/value。
How is it possible that files can contain null bytes in operating systems written in a language with null-terminating strings (namely, C)?
有了上面的陈述,这个问题就变成了,一个文件怎么能包含 0? 现在答案很简单了。
For example, if I run this shell code:
$ printf "Hello[=12=], World!" test.txt $ xxd test.txt 0000000: 4865 6c6c 6f00 2c20 576f 726c 6421 Hello., World!
I see a null byte in test.txt (at least in OS X). If C uses null-terminating strings, and OS X is written in C, then how come the file isn't terminated at the null byte, resulting in the file containing
Hello
instead ofHello[=21=], World!
?Is there a fundamental difference between files and strings?
假设一个ASCII字符编码(1-byte/8-bit个字符在0和127的十进制范围内):
- 字符串 是 buffers/char-arrays 个 1 字节字符(其中 NULL = 0_decimal 和 '0' = 48_decimal))。
- 文件 是 32 位或 64 位序列“WORDS”(取决于 OS 和硬件,即分别为 x86 或 x64)。
因此,一个仅包含 ASCII 字符串的 32 位 OS 文件将是一个 32 位(4 字节)字序列该范围在十进制值 0 和 127 之间,基本上只使用 4 字节字的第一个字节(b2:base-2,十进制是 base-10 和十六进制 base-16,fyi)
0_b2: 00000000 00000000 00000000 00000000
32_b2: 00000000 00000000 00000000 00100000
64_b2: 00000000 00000000 00000000 01000000
96_b2: 00000000 00000000 00000000 01100000
127_b2: 00000000 00000000 00000000 11111111
128_b2: 00000000 00000000 00000001 00000000
这个字节是 left-most 还是 right-most 取决于 OS 的 endianness.
但是为了回答你关于 Hello[=21=], World!
之后缺少的 NULL
的问题,我假设它被 EOL/EOF[=91= 取代了](文件结尾)值,这很可能是 non-printable,这就是为什么您在输出中看不到它的原因 window.
注意: 我确定现代 OS(和经典的基于 Unix 的系统)优化了 ASCII 的存储个字符,这样1个字(4个字节)可以打包成4个字符。 UTF 但是,由于这些编码使用更多位来存储字符,因此它们有更大的 alphabets/character 集来表示(例如50k Kanji/Japanese 个字符)。我认为 UTF-8 类似于 ASCII,为了统一而重命名(与UTF-16 和 UTF-32).
注意: C/C++ 实际上使用字符数组(即字符串)将 "pack" 4 个字符放入一个 4 字节的单词中。由于每个 char 都是 1 字节,因此编译器将在堆栈或堆上分配并在算术上将其视为 1 字节。所以如果你在一个函数中声明一个数组(即一个auto-variable),像这样
char[] str1[7] = {'H','e','l','l','o','!','[=14=]'};
函数堆栈从地址 1000_b10 (base-10/decimal) 开始,然后你有:
072 101 108 108 111 033
addr char binary decimal
---- ----------- -------- -------
1000: str1[0] 'H' 01001000 (072)
1001: str1[1] 'e' 01100101 (101)
1002: str1[2] 'l' 01101100 (108)
1003: str1[3] 'l' 01101100 (108)
1004: str1[4] 'o' 01101111 (111)
1005: str1[5] '!' 00100001 (033)
1006: str1[6] '0' 00000000 (000)
由于 RAM 是 byte-addressable,每个地址引用一个字节。
考虑将数据写入文件的常用 C 函数调用 — write(2)
:
ssize_t
write(int fildes, const void *buf, size_t nbyte);
…和fwrite(3)
:
size_t
fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
这些函数都不接受 const char *
NUL-terminated 字符串。相反,它们采用具有明确大小的字节数组(const void *
)。这些函数像处理任何其他字节值一样处理 NUL 字节。