【问题标题】:How can a file contain null bytes?文件如何包含空字节?
【发布时间】:2016-01-05 20:50:31
【问题描述】:

在使用以空字符结尾的字符串(即 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!?文件和字符串有根本区别吗?

【问题讨论】:

  • 重复使用fputc(0, ostream)fprintf(ostream, "%c", 0),很容易创建一个包含许多空字符的文件。
  • 字符串不是唯一可以写入文件的东西。
  • 注意:许多非 C 语言,但其解释器是用 C 编写的,仍然允许字符串中的空字节。 (例如 Lua)
  • 您假设所有文件都是文本文件。这是为什么呢?
  • @SamusArin Byte 是标准库中可用的最小数量,非标准库可以很好地处理比特或其他任何东西。

标签: c macos null-terminated


【解决方案1】:

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

注意: 根据 n.m.(请参阅 OP 中的评论)“字节 是可写入磁盘的最小数量 使用 C 标准库,非标准库可以很好地处理位或其他任何东西。”所以我在下面所说的关于 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 的数字编码/值。

文件怎么可能在操作中包含空字节 用带有空终止字符串的语言编写的系统(即, 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”的序列(取决于操作系统和硬件,即分别为 x86 或 x64)。

因此,仅包含 ASCII 字符串的 32 位操作系统文件将是 32 位(4 字节)字的序列,范围在十进制值 0 和 127 之间,本质上仅使用 4 字节字的第一个字节(b2:base-2,十进制为 base-10,十六进制为 base-16,仅供参考)

  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 的问题,我将假设它已被 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(base-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 是可字节寻址的,因此每个地址都引用一个字节。

【讨论】:

  • 为什么任何体面的程序员都需要用(拼写错误的)侮辱和指向他们自己答案的链接向其他人(不相关的)答案发送垃圾邮件。建议您在被暂停之前不要这样做:)你关于 UTF-8 的陈述和你关于 EOF 转换的假设一样是完全错误的。您根本没有非常清楚地回答这个相当直截了当的问题。
  • 这不是拼写错误。还有另外 4 个无意的(如果您忽略所有黑帮说唱拼写)。 Your = You're,finder = finer,point's = points,programmer = programming。建议你在 SO 上玩得很好,否则你不会持续很长时间 :)
  • 答案是什么????充满了不相关和错误/太模糊的东西。一:我认为UTF-8类似于ASCII。这些几乎是无关的东西。 UTF-8 是一种编码系统,ASCII 是一种字符集。有一些关系,但不是这种关系。你的第二个笔记呢????
  • ASCII 'd' 告诉您与之关联的值是什么,而不是该值必须如何编码! UTF-8 告诉您值必须如何在机器中表示(可变多字节编码)。其次,char 是一个字节长,所以 str[1] 不可能是 1008,不是吗?而OP的根本问题与字符编码无关,而与ASCII NUL在各个地方(文件和字符串)的作用有关
  • @Jean-BaptisteYunès en.wikipedia.org/wiki/UTF-8:“UTF-8 是一种字符编码……它旨在向后兼容 ASCII……”
【解决方案2】:

Null-terminated strings 是一种 C 构造,用于确定打算用作字符串的字符序列的结尾。 strcmpstrcpystrchr 等字符串操作函数和其他函数使用此构造来执行其职责。

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

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

#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 个用于“Hello”,1 个用于显式空字节,5 个用于“World”,1 个用于隐式结束字符串常量的空字节。由于fwrite 被赋予了数组的完整大小(12),它会写入所有这些字节。事实上,查看文件内容,我们可以看到其中的每一个字节。

附带说明一下,在每个fwrite 调用中,我都硬编码了第三个参数的数组大小,而不是使用更动态的表达式,例如sizeof(a1)/sizeof(a1[0]),以便更清楚地知道有多少个在每种情况下都写入字节。

【讨论】:

  • "NULL" 最适合用于描述 空指针常量NULL。推荐使用“空字符”或`'\0'``来描述字符串的终止字符。
  • ascii(7) 将其称为NUL 大写,一个LNULL(void *)0 的另一个名称。 Null 是英语中的一个词,通常含义有些模棱两可。
  • 更准确地说,您不能将它们视为 C 字符串。 C 字符串并不是唯一可能的字符串表示形式。
  • @Aron:示例(请)?
  • @SamusArin 例如,UTF-16,如果您将标准 ASCII C-String 转换为 UTF-16 字符串,则每隔一个字节将为空。
【解决方案3】:

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

就 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”的文件,并将 10 个字节写入其中。请注意,虽然buffer 是一个字符数组,但它不是一个以空字符结尾的字符串。

【讨论】:

  • @WeatherVane 谢谢!那一定是八进制十:-)
  • 顺便说一句:对于10(octal)10(hexadecimal),是否有任何相当于"ten" = 10(decimal) 的词?
  • @WeatherVane 我不知道,但这对于english.stackexchange.com 来说可能是一个好问题:-)
  • @WeatherVane 10(octal) 的单词是“八”。 10(hexadecimal) 的单词是“十六”。 “一个零”也有效(对于任何一个),但不是“十”。
  • 请注意,在类 UNIX 系统(POSIX 系统)上,打开模式下的 b 无关紧要。在 Windows 系统上,b 真的很重要;它会影响读取数据时对回车符和 control-Z 字符的解释,以及写入数据时对换行符的解释。
【解决方案4】:

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

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

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

【讨论】:

    【解决方案5】:

    虽然空字节用于终止字符串并且是字符串操作函数所必需的(因此它们知道字符串的结束位置),但在二进制文件中 \0 字节可以无处不在。

    例如,考虑一个具有 32 位数字的二进制文件,如果它们的值小于 2^24,它们都将包含空字节(例如:0x001a00c7 或 64 位 0x0000000a00001a4d)。

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

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

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

    【讨论】:

    • 关于 UTF-16 的要点。然而,ASCII 是否有前导 '\0' 或尾随 '\0' 取决于文件是否为 UTF-16LE or UTF-16BE
    • @chux - 事实上,二进制数也是如此。我编辑了答案。谢谢。
    【解决方案6】:

    考虑将数据写入文件的常用 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 字节视为任何其他字节值。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-09-02
      • 2018-04-25
      • 2012-08-08
      • 1970-01-01
      • 1970-01-01
      • 2019-03-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多