如果文件在用空终止字符串(即C)的语言编写的操作系统中可以包含空字节怎么可能?
例如,如果我运行此shell代码:
$ printf "Hello\00, 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\00, World!
?文件和字符串之间是否有根本区别?
答案 0 :(得分:44)
以空值终止的字符串是一种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
函数并告诉它写出int
大小的4个元素,所以数组中的所有值都出现在文件中。您可以从输出中看到所有值都已写入,值为32位,并且每个值都以little-endian字节顺序写入。我们还可以看到数组的第二个和第四个元素每个都包含一个空字节,而第三个值为0则有4个空字节,并且都出现在文件中。
我们还在第二个数组上使用fwrite
,其中包含char
类型的元素,我们再次看到所有数组元素都出现在文件中。特别是,数组中的第三个值是0,它由一个也出现在文件中的空字节组成。
第一个数组首先用fprintf
函数编写,使用%s
格式说明符,它需要一个字符串。它在遇到空字节之前将此数组的前5个字节写入文件,之后它将停止读取数组。然后按照格式打印换行符(0x0a
)。
它再次写入文件的第三个数组,这次使用fwrite
。字符串常量"Hello\x0World"
包含12个字节:5用于&#34; Hello&#34;,一个用于显式空字节,5用于&#34; World&#34;以及一个用于隐含结束的空字节字符串常量。由于fwrite
被赋予数组(12)的完整大小,因此它会写入所有这些字节。实际上,查看文件内容,我们会看到每个字节。
作为旁注,在每个fwrite
调用中,我已经为第三个参数硬编码了数组的大小,而不是使用更动态的表达式,例如sizeof(a1)/sizeof(a1[0])
来制作它更清楚确切地说每种情况下写入了多少字节。
答案 1 :(得分:21)
以空值终止的字符串肯定是不是你可以放入文件的唯一东西。操作系统代码不认为文件是存储以空字符结尾的字符串的工具:操作系统将文件显示为任意字节的集合。
就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
是一个字符数组,但不是一个以空字符结尾的字符串。
答案 2 :(得分:8)
因为文件只是一个字节流,任何字节包括空字节。有些文件只包含所有可能字节的子集时称为文本文件:可打印的文件(大致字母数字,空格,标点符号)。
C字符串是由空字节终止的字节序列,只是常规问题。它们往往是混乱的根源;只是一个以null结尾的序列,意味着任何以null结尾的非空字节都是正确的C字符串!甚至包含不可打印字节或控件字符的字符。要小心,因为你的例子不是C的!在C printf("dummy\000foo");
中永远不会打印foo
,因为printf
将考虑从d
开始并在中间的空字节结束的C字符串。一些编译器抱怨这样的C字符串文字。
现在C字符串(通常也只包含可打印的字符)和文本文件之间没有直接链接。将C字符串打印到文件中通常只包含存储非空字节的子序列。
答案 3 :(得分:5)
虽然null-bytes用于终止字符串并且需要字符串操作函数(因此它们知道字符串结束的位置),但在二进制文件\0
中,字节可以无处不在。
例如,考虑一个具有32位数字的二进制文件,如果它们的值小于2 ^ 24,它们都将包含空字节(例如:0x 00 1a 00 < / strong> c7,或64位0x 000000 0a 0000 1a4d)。
Unicode-16的同义词,其中所有ASCII字符的前导或尾随\0
,具体取决于endianness,字符串需要以\0\0
结尾。
许多文件甚至用\0
个字节填充块(到4kB甚至64kB),以便快速访问所需的块。
对于文件中的更多空字节,请查看sparse files,默认情况下所有字节都为\0
,并且甚至存储的空字节块也不存在磁盘节省空间。
答案 4 :(得分:0)
考虑将数据写入文件的常用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终止的字符串。相反,它们采用具有显式大小的字节数组(const void *
)。这些函数处理NUL字节就像任何其他字节值一样。
答案 5 :(得分:-3)
在回答任何问题之前,请注意
(注意:根据nm(参见OP中的评论)&#34; 字节 是可写出的最小数量使用C标准库到磁盘 ,非标准库可能很好地处理位或其他任何东西。&#34;所以我在下面说的WORD大小是最小的数量可能是不是很正确,但仍然提供了洞察力。)
NULL始终为0_decimal (几乎)
dec: 0
hex: 0x00000000
bin: 00000000 00000000 00000000 00000000
虽然它的实际值是由编程语言的规范定义的,但是使用定义的常量NULL
而不是硬编码0
到处(如果它发生变化,当地狱冻结时)结束)。
ASCII编码字符&#39; 0&#39;是48_decimal
dec: 48
hex: 0x00000030
bin: 00000000 00000000 00000000 00110000
NULL
的概念并不存在于文件中,而是存在于生成应用程序的编程语言中。只有NULL
的数字编码/值存在于文件中。
文件如何在操作中包含空字节 用一种带有空终止字符串的语言编写的系统(即 C)?
根据上面提到的这个问题, 文件如何包含0? 答案现在很简单。
例如,如果我运行此shell代码:
$ printf "Hello\00, 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\00, World!
?文件和字符串之间是否有根本区别?
假设 ASCII 字符编码(小数范围为0到127的1字节/ 8位字符):
因此,仅包含 ASCII 字符串的32位操作系统文件将是32位(4字节)字的序列,其范围在十进制值0和127之间,基本上只使用4字节字的第一个字节(b2:base-2,十进制是base-10和hex 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
此字节的天气最左侧或最右侧取决于操作系统的字节序。
但要在NULL
之后回答关于遗失Hello\00, World!
的问题,我将假设它被 EOL / EOF 取代(文件末尾) )值,这很可能是不可打印的,这就是为什么你没有在输出窗口中看到它。
注意:我确信现代操作系统(以及经典的基于Unix的系统)可以优化 ASCII 字符的存储,所以1个字(4个字节)可以打包4个字符。然而,事情随着 UTF 而变化,因为这些编码使用更多位来存储字符,因为它们具有更大的字母/字符集来表示(例如50k汉字/日文字符)。我认为 UTF-8 类似于 ASCII ,并重新命名为一致性(使用 UTF-16 和<强> UTF-32 强>)。
注意:事实上C / C ++确实&#34;打包&#34;使用字符数组(即字符串)将4个字符转换为单个4字节字。由于每个char都是1字节,因此编译器将在堆栈或堆上分配并将其视为1字节(算术)。所以如果你在一个函数中声明一个数组(即一个自动变量),就像这样
char[] str1[7] = {'H','e','l','l','o','!','\0'};
函数堆栈从地址1000_b10(基数为10 /十进制)开始,然后你有:
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是字节可寻址的,因此每个地址都引用一个字节。