文件如何包含空字节?

文件如何在用空终止字符串(即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! ? 文件和字符串之间是否存在根本区别?

以空值终止的字符串是一种C结构,用于确定要用作字符串的字符序列的结尾。 字符串操作函数(如strcmpstrcpystrchr等)使用此构造来执行其职责。

但是,您仍然可以在程序中以及文件中读取和写入包含空字节的二进制数据。 你不能把它们视为字符串。

这是一个如何工作的例子:

 #include  #include  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])来使其更多清楚地确定每种情况下写入的字节数。

以空值终止的字符串当然不是您可以放入文件的唯一内容。 操作系统代码不认为文件是存储以空字符结尾的字符串的工具:操作系统将文件呈现为任意字节的集合。

就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结尾的字符串。

因为文件只是一个字节流,包括空字节在内的任何字节。 有些文件只包含所有可能字节的子集时称为文本文件:可打印的文件(大致字母数字,空格,标点符号)。

C字符串是由空字节终止的字节序列,只是常规问题。 它们往往是混乱的根源; 只是一个以null结尾的序列,意味着任何以null结尾的非空字节都是正确的C字符串! 甚至包含不可打印字节或控件字符的字符。 要小心,因为你的例子不是C的! 在C printf("dummy\000foo"); 永远不会打印foo因为printf会考虑从d开始到中间空字节结束的C字符串。 一些编译器抱怨这样的C字符串文字。

现在C字符串(通常也只包含可打印的字符)和文本文件之间没有直接链接。 将C字符串打印到文件中通常只包括存储非空字节的子序列。

虽然null-bytes用于终止字符串并且需要字符串操作函数(因此它们知道字符串结束的位置),但在二进制文件中\0字节可以在任何地方。

例如,考虑具有32位数字的二进制文件,如果它们的值小于2 ^ 24,则它们都将包含空字节(例如:0x 00 1a 00 c7或64位0x 000000 0a 0000 1a4d)。

同义词为Unicode-16,其中所有ASCII字符都有一个前导或尾随\0 ,具体取决于它们的字节顺序 ,字符串需要以\0\0结尾。

许多文件甚至用\0字节填充块(到4kB甚至64kB),以便快速访问所需的块。

对于文件中甚至更多的空字节,请查看稀疏文件 ,其中所有字节默认为\0 ,并且甚至没有将存储空字节的块存储在磁盘上以节省空间。

考虑通常的C函数调用将数据write(2)文件 – 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字节就像任何其他字节值一样。

在回答任何问题之前,请注意

注意:根据nm(参见OP中的注释)“ 字节 是可用 C标准库 写入磁盘的最小数量 ,非标准库可能很好地处理位或其他任何东西。”所以我在下面说关于WORD大小是最小的数量可能不是很正确,但仍然提供了洞察力)。

NULL始终为0_decimal实际上

 dec: 0 hex: 0x00000000 bin: 00000000 00000000 00000000 00000000 

虽然它的实际值是由编程语言的规范定义的,但是使用定义的常量NULL而不是硬编码0到处(如果它改变,当地狱冻结时)。

字符’0’的ASCII编码是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位字符):

  • 字符串是1字节字符的缓冲区/字符数组(其中NULL = 0_decimal和’0’= 48_decimal))。
  • 文件是32位或64位“ WORDS ”的序列(取决于OS和硬件,分别是x86或x64)。

因此,仅包含ASCII字符串的32位OS文件将是一个32位(4字节)字序列,其范围在十进制值0和127之间,基本上只使用4字节字的第一个字节( b2:base-2,decimal是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 

此字节的天气最左侧或最右侧取决于操作系统的字节序

但要回答你关于Hello\00, World!之后缺少NULL问题Hello\00, World! 我将假设它被EOL / EOF (文件结束)值替代,这很可能是不可打印的,这就是为什么你没有在输出窗口中看到它。

注意:我确信现代操作系统(和基于Unix的经典系统)可以优化ASCII字符的存储,因此1个字(4个字节)可以打包4个字符。 然而事情随着UTF而改变,因为这些编码使用更多的比特来存储字符,因为它们具有更大的字母/字符集来表示(例如50k汉字/日文字符)。 我认为UTF-8类似于ASCII ,并且重新命名为一致性(使用UTF-16UTF-32 )。

注意:事实上,C / C ++确实使用字符数组(即字符串)将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是字节可寻址的,因此每个地址都引用一个字节。