【问题标题】:What happens if I write less than 12 bytes to a 12 byte buffer?如果我向 12 字节缓冲区写入少于 12 个字节会发生什么?
【发布时间】:2019-02-20 21:58:48
【问题描述】:

可以理解,遍历缓冲区会出错(或造成溢出),但是如果 12 字节缓冲区中使用的字节数少于 12 会发生什么情况?有可能还是空尾总是用0填充?可能有帮助的正交问题:当缓冲区被实例化但尚未被应用程序使用时,缓冲区中包含什么?

我在 Visual Studio 中查看了一些宠物程序,似乎它们都附加了 0(或空字符),但我不确定这是否是可能因语言/编译器而异的 MS 实现。

【问题讨论】:

  • memset 可用于确保缓冲区初始化为零。
  • @TruBlu:或者在 C++ 中,std::fill
  • @TruBlu 不要那样做,我见过很多人做 malloc 后跟 memset,或 char foo[X] 后跟 memset,没有充分的理由。如果您希望它们零初始化,请使用 calloc() 而不是 malloc(),或者使用 char foo[x]={0};,它将被零初始化。
  • 定义“缓冲区”。一般来说,12 字节数组不是我称之为 12 字节缓冲区的数据结构。
  • @hanshenrik 很高兴知道。感谢您提供优化的替代方案。 malloc()calloc() 更有效,因此它是分配内存的首选方法除非需要零初始化。

标签: c++ c memory-management buffer


【解决方案1】:

以下面的例子(在代码块内,不是全局的):

char data[12];
memcpy(data, "Selbie", 6);

甚至这个例子:

char* data = new char[12];
memcpy(data, "Selbie", 6);

在上述两种情况下,data 的前 6 个字节为Selbiedata 的剩余 6 个字节被视为“未指定”(可以是任何值)。

是否有可能或者空尾总是用 0 填充?

完全不保证。我所知道的唯一能保证零字节填充的分配器是calloc。示例:

char* data = calloc(12,1);  // will allocate an array of 12 bytes and zero-init each byte
memcpy(data, "Selbie");

当缓冲区被实例化但尚未被应用程序使用时,缓冲区中包含什么?

从技术上讲,根据最新的 C++ 标准,分配器传递的字节在技术上被认为是“未指定的”。您应该假设它是垃圾数据(任何东西)。不对内容做任何假设。

使用 Visual Studio 的调试版本通常会使用 0xcc0xcd 值初始化缓冲区,但在发布版本中并非如此。然而,Windows 和 Visual Studio 的编译器标志和内存分配技术可以保证零初始化内存分配,但它不可移植。

【讨论】:

  • "data 的剩余 6 个字节未定义,但将是一些东西。"但是,如果它们是未定义的,那么尝试确定“某事”不是未定义的行为吗?所以这并不重要,唯一的解决方案是永远不要读取未初始化的内存。这不是“随机”的情况(尤其不是作为 RNG);相反,我会假设未初始化的数据是有毒的。读取 char 类型可能有一个例外,它不能有陷阱表示或填充,但进入读取未初始化部分的情况仍然没有意义或好的代码。
  • "你应该假设它可以被随机字节填充。" - 这个答案很好,但我想反对使用“随机”这个词。分配内存然后读取它不是随机性的良好来源。
  • 另一个零填充的“分配器”用于具有静态链接的对象。在 Unix 的说法中,它们被分配在 BSS 段中 - 空白静态存储。目标文件只需要指定这些变量的位置和长度,因为运行时加载器将用零填充它们。哦,值得一提的是内存检查器可以检测读取未初始化内存的尝试 - 我通常推荐 Valgrind,但在 Windows 平台上可能还有其他选择。
  • "剩余的 6 字节数据是未定义的,但会是一些东西。" - 不,这是错误的!在未定义的行为中访问未初始化的值。您不能在“好吧,那里有东西,我不在乎,没关系”的假设下编写代码。假设不会访问未初始化的值,优化器可能会完全重新排列您的代码。
  • @Wilson 这是一个任意值(不过,它很容易在视觉上识别)。 Different values have different meanings. 原因是在调试过程中向开发人员提示出了什么问题(或者简单地说,哪些变量尚未初始化)。
【解决方案2】:

考虑你的缓冲区,用零填充:

[00][00][00][00][00][00][00][00][00][00][00][00]

现在,让我们向它写入 10 个字节。从 1 开始递增的值:

[01][02][03][04][05][06][07][08][09][10][00][00]

现在又是 4 次 0xFF:

[FF][FF][FF][FF][05][06][07][08][09][10][00][00]

如果 12 字节缓冲区中使用的字节数少于 12 字节会怎样?有可能还是空尾总是用0填充?

你想写多少就写多少,剩下的字节不变。

可能有帮助的正交问题:缓冲区中包含什么 它已实例化但尚未被应用程序使用?

未指定。预计之前使用此内存的程序(或程序的其他部分)会留下垃圾。

我在 Visual Studio 中查看了一些宠物程序,似乎它们都附加了 0(或空字符),但我不确定这是否是可能因语言/编译器而异的 MS 实现。

这正是你所想的。这次有人为你做了这件事,但不能保证它会再次发生。它可能是附加清理代码的编译器标志。某些版本的 MSVC 在调试时使用 0xCD 填充新内存,但不是在发布时。它也可以是一种系统安全功能,可在将内存提供给您的进程之前擦除内存(因此您无法监视其他应用程序)。永远记得使用memset 在重要的地方初始化你的缓冲区。最后,如果您依赖新缓冲区来包含某个值,请在自述文件中强制使用某些编译器标志。

但清洁并不是真正必要的。您需要一个 12 字节长的缓冲区。你用 7 个字节填充它。然后你将它传递到某个地方 - 你说“这里有 7 个字节给你”。从缓冲区读取时,缓冲区的大小无关紧要。你期望其他函数能读到你写的一样多,而不是尽可能多。事实上,在 C 中,通常无法判断缓冲区的长度。

附注:

可以理解,检查缓冲区错误(或造成溢出)

没有,这就是问题所在。这就是为什么它是一个巨大的安全问题:没有错误并且程序试图继续,所以它有时会执行它从未想过的恶意内容。所以我们不得不向操作系统添加一堆机制,比如 ASLR,这将增加程序崩溃的可能性并降低它继续损坏内存的可能性。所以,永远不要依赖那些事后考虑的守卫,自己注意缓冲区边界。

【讨论】:

  • 您可能想要添加这些精度:在输入 main 之前,具有静态持续时间的数组被初始化为 0。其他数组,无论是具有自动存储的本地值还是使用malloc() 从堆中分配的数组都具有未指定的内容,将这些内容作为字节读取是可以的,但对于大多数其他类型具有未定义的行为。由calloc() 分配的数组被初始化为所有位为零,这与初始化为0 略有不同。
【解决方案3】:

C++ 的存储类包括全局、自动和静态。初始化取决于变量的声明方式。

char global[12];  // all 0
static char s_global[12]; // all 0

void foo()
{
   static char s_local[12]; // all 0
   char local[12]; // automatic storage variables are uninitialized, accessing before initialization is undefined behavior 
}

一些有趣的细节here

【讨论】:

  • 讨论这个很累,因为错误信息比比皆是,但就标准而言,local 不是充满随机垃圾,而是充满了鼻恶魔。 (读取一个未初始化的变量是完整的UB。)
  • 更新更清楚自动变量在初始化之前是未定义的
【解决方案4】:

程序知道字符串的长度,因为它以空终止符结束,一个值为零的字符。

这就是为什么为了在缓冲区中适应字符串,缓冲区必须比字符串中的字符数至少长 1 个字符,以便它也可以适应字符串加上空终止符。

缓冲区中之后的任何空间都保持不变。如果以前有数据,它仍然存在。这就是我们所说的垃圾。

假设这个空间是零填充的只是因为你还没有使用它是错误的,在你的程序到达那个点之前你不知道那个特定的内存空间是用来做什么的。应将未初始化的内存视为随机且不可靠的内存。

【讨论】:

  • 这里和上面一样:读取未初始化的内存会导致未定义的行为,它不是用“随机”值填充的。
  • @ArneVogel 哦,这根本不是随机的,但它应该像其中的随机和不可靠一样处理
【解决方案5】:

前面的所有答案都很好也很详细,但是 OP 对 C 编程来说似乎是新手。所以,我认为一个真实世界的例子可能会有所帮助。

假设您有一个可以容纳六瓶的纸板饮料架。它一直在你的车库里闲逛,所以它不是六瓶,而是堆积在车库角落里的各种令人讨厌的东西:蜘蛛、老鼠屋等。

分配后的计算机缓冲区有点像这样。你不能确定里面有什么,你只知道它有多大。

现在,假设您在支架中放了四个瓶子。你的持有人没有改变大小,但你现在知道四个空间里有什么。另外两个空间,连同其可疑的内容,仍然存在。

计算机缓冲区也是如此。这就是为什么您经常看到一个 bufferSize 变量来跟踪有多少缓冲区正在使用中。更好的名称可能是 numberOfBytesUsedInMyBuffer,但程序员往往非常简洁。

【讨论】:

    【解决方案6】:

    写入缓冲区的一部分不会影响缓冲区中未写入的部分;它将包含事先存在的任何内容(这自然完全取决于您最初是如何获得缓冲区的)。

    正如其他答案所述,静态和全局变量将初始化为0,但不会初始化局部变量(而是包含事先在堆栈上的任何内容)。这符合零开销原则:在某些情况下,初始化局部变量会产生不必要的运行时开销,而静态和全局变量在加载时作为数据段的一部分进行分配。

    堆存储的初始化由内存管理器选择,但通常也不会被初始化。

    【讨论】:

      【解决方案7】:

      一般来说,缓冲区不足并不罕见。分配比所需更大的缓冲区通常是一种好习惯。 (试图总是计算一个准确的缓冲区大小是一个常见的错误来源,而且通常是浪费时间。)

      当缓冲区比它需要的大时,当缓冲区包含的数据少于其分配的大小时,跟踪那里有多少数据 显然很重要。通常有两种方法:(1) 使用显式计数,保存在单独的变量中,或 (2) 使用“哨兵”值,例如标记字符串结尾的 \0 字符C.

      但问题是,如果不是所有缓冲区都在使用,那么未使用的条目包含什么?

      当然,一个答案是,没关系。这就是“未使用”的意思。您关心所使用条目的值,这些值由您的计数或哨兵值计算。您不关心未使用的值。

      基本上有四种情况可以预测缓冲区中未使用条目的初始值:

      1. 当您分配具有static 持续时间的数组(包括字符数组)时,所有未使用的条目都被初始化为 0。

      2. 当你分配一个数组并给它一个显式初始化器时,所有未使用的条目都被初始化为 0。

      3. 当你调用calloc时,分配的内存被初始化为all-bits-0。

      4. 当您调用strncpy 时,目标字符串将被填充为n 大小,并带有\0 字符。

      在所有其他情况下,缓冲区的未使用部分是不可预测的,并且通常包含它们上次所做的任何事情(无论这意味着什么)。特别是,您无法预测具有自动持续时间的未初始化数组的内容(即,一个函数的本地数组并且未使用static 声明),并且您无法预测使用malloc 获得的内存内容。 (有时,在这两种情况下,内存在第一次开始时往往是全零,但你绝对不想依赖这个。)

      【讨论】:

      • 关于strncpy 的好点:我很想赞成向用户​​介绍一个鲜为人知的副作用,但也想反对暗中提倡使用这个容易出错的功能,太糟糕了,我不能两者都做,所以我一个都不做。
      【解决方案8】:

      这取决于存储类说明符、您的实现及其设置。 一些有趣的例子: - 未初始化的堆栈变量可以设置为0xCCCCCCCC - 未初始化的堆变量可以设置为0xCDCDCDCD - 未初始化的静态或全局变量可以设置为0x00000000 - 或者它可能是垃圾。 对此做出任何假设都是有风险的。

      【讨论】:

        【解决方案9】:

        我认为正确的答案是您应该始终跟踪写入了多少个字符。 与读取和写入等低级函数一样,需要或给出读取或写入的字符数。以同样的方式 std::string 跟踪其实现中的字符数

        【讨论】:

          【解决方案10】:

          没有指定初始值设定项的静态持续时间的声明对象(那些在函数外部声明的对象,或带有static 限定符的对象)将被初始化为由文字零表示的任何值[即一个整数零、浮点零或空指针,视情况而定,或包含此类值的结构或联合]。如果任何对象(包括自动持续时间的对象)的声明包含初始化程序,则其值由该初始化程序指定的部分将按指定设置,其余部分将与静态对象一样归零。

          对于没有初始化器的自动对象,情况有点模糊。给定类似的东西:

          #include <string.h>
          
          unsigned char static1[5], static2[5];
          
          void test(void)
          {
            unsigned char temp[5];
            strcpy(temp, "Hey");
            memcpy(static1, temp, 5);
            memcpy(static2, temp, 5);
          }
          

          标准很清楚test 不会调用未定义行为,即使它复制了未初始化的temp 部分。至少从 C11 开始,标准的文本还不清楚是否对 static1[4]static2[4] 的值有任何保证,最值得注意的是它们是否可能保持不同的值。一份缺陷报告指出,该标准并非旨在禁止编译器表现得好像代码是这样的:

          unsigned char static1[5]={1,1,1,1,1}, static2[5]={2,2,2,2,2};
          
          void test(void)
          {
            unsigned char temp[4];
            strcpy(temp, "Hey");
            memcpy(static1, temp, 4);
            memcpy(static2, temp, 4);
          }
          

          这可能会使static1[4]static2[4] 保持不同的值。该标准没有说明用于各种目的的质量编译器是否应该在该函数中运行。如果程序员要求 static1[4]static2[4] 保持相同的值,但不关心该值是什么,则该标准也没有提供有关如何编写函数的指导。

          【讨论】:

            猜你喜欢
            • 2020-07-30
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-07-23
            • 1970-01-01
            • 1970-01-01
            • 2020-04-04
            • 1970-01-01
            相关资源
            最近更新 更多