如果我向一个12字节的缓冲区中写入少于12个字节会发生什么?

28

可以理解的是,如果超过缓冲区大小会出错(或创建溢出),但如果12字节缓冲区中只使用了不到12个字节,会发生什么情况?是否可能或者空的后续总是填充0?一个有关的问题:当应用程序尚未使用时,实例化的缓冲区中包含什么内容?

我看了一些在Visual Studio中的小程序,它们似乎附加了0(或空字符),但我不确定这是否是Microsoft实现的,可能在不同的语言/编译器中会有所不同。


1
memset 可以用来确保缓冲区被初始化为零。 - TruBlu
4
在C++中,可以使用std::fill函数实现相同的功能。 - MSalters
4
@TruBlu 不要那样做,我看过很多人先使用 malloc 然后再使用 memset,或者 char foo[X] 后跟 memset,没有好的理由。如果你想要它们被初始化为零,可以使用 calloc() 替代 malloc(),或者使用 char foo[x]={0}; 来进行零初始化。 - hanshenrik
1
定义“缓冲区”。一般来说,一个12字节的数组不是我所称之为12字节缓冲区的数据结构。 - Tom Blodget
2
@hanshenrik 很好知道。感谢您提供优化的替代方案。malloc()calloc()更有效率,因此在分配内存时它是首选方法,除非需要零初始化。 - TruBlu
@TruBlu,你说得非常准确,这正是为什么malloc和calloc并存的原因(而不是将它们中的一个作废)。 - hanshenrik
10个回答

18

举个例子(在代码块内,而不是全局范围内):

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

甚至包括这个例子:

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

在上述两种情况中,data 的前6个字节分别是 S, e, l, b, ie。剩余的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 中有编译器标志和内存分配技术,可以保证零初始化内存分配,但这不是可移植的。


4
“data” 的剩余 6 个字节是未定义的,但会变成某些东西。但如果它们未定义,试图确定“某些东西”不就是未定义行为吗?所以这并不重要,唯一的解决方案是永远不要读取未初始化的内存。这不是“随机性”的问题(特别是不是 RNG);相反,我会说假定未初始化的数据是“有毒的”。这可能有一个例外情况,即读取 char 类型,因为它们不能有陷阱表示或填充,但仍然读取未初始化部分是没有意义或不良代码的情况。 - underscore_d
11
你应该假定它可能会被填充以随机字节。但是,我要反对使用“随机”这个词。分配内存然后读取它并不是一个好的随机源。 - ymbirtt
6
剩余的6个字节数据未定义,但将会有一些东西。 - 不,这是错误的!访问未初始化的值是未定义的行为。你不能假设“嗯,那里面有些东西,我不在乎是什么,这没有关系”,然后编写代码。优化器可能会完全重新排列您的代码,假定不会发生对未初始化值的访问。 - Sebastian Redl
strncpy(data, "Selbie", 12)也会用0填充缓冲区的其余部分。 - ilkkachu
2
@Wilson 这是一个任意的值(虽然在视觉上很容易识别)。不同的值有不同的含义。 原因是在调试期间给开发人员提供提示,指出出了什么问题(或者仅仅是哪些变量尚未被初始化)。 - Arne Vogel
显示剩余4条评论

11

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 
}

这里有一些有趣的细节.


讨论这个话题很累人,因为充斥着错误信息,但就标准而言,“local”并不是填充了随机垃圾,而是填充了鼻子里的恶魔。(读取未初始化的变量是完全未定义的行为。) - Arne Vogel
更新为更清晰,即自动变量在初始化之前是未定义的。 - Matthew Fisher

11

考虑一下你的缓冲区,它被填充了零:

[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来初始化缓冲区。最后,如果您依赖于新缓冲区包含某个值,请在readme中强制使用特定的编译器标志。

但是清理并不是必要的。您拿了一个12字节长的缓冲区。您用7个字节填充它。然后将其传递到某个地方-您说“这里有7个字节供您使用”。读取缓冲区时,缓冲区的大小与其无关。您期望其他函数读取您所写的内容,而不是尽可能多地读取。实际上,在C中通常无法确定缓冲区的长度。

另外一件事:

可以理解为超过缓冲区会出错(或创建溢出)

这并不是真的,这就是问题所在。这就是为什么它是一个巨大的安全问题:没有错误,并且程序试图继续运行,因此它有时会执行它从未打算执行的恶意内容。因此,我们必须向操作系统添加一堆机制,比如ASLR,这将增加崩溃程序的可能性并减少继续使用损坏内存的可能性。因此,永远不要依赖这些事后防护措施,并自己观察您的缓冲区边界。


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

4
程序知道字符串的长度,因为它在字符串末尾放置了一个值为零的空终止字符。
这就是为什么为了容纳一个字符串在缓冲区中,缓冲区大小必须比字符串中的字符数多至少1个字符,以便能够容纳字符串和空终止字符。
此后缓冲区中的任何空间都不会被触及。如果之前有数据,那么数据仍然存在。我们将此称为垃圾。
假设这个空间被填充为零是错误的,只因为你还没有使用它,你不知道在你的程序到达这一点之前这个特定的内存空间被用于什么目的。未初始化的内存应被视为随机和不可靠的内容。

同样适用于此处:读取未初始化的内存会导致未定义的行为,它不会填充“随机”值。 - Arne Vogel
@ArneVogel 哦,它根本不是随机的,但是应该将其处理为随机和不可靠的内容。 - Havenard

3
所有之前的回答都非常好,也非常详细,但是问问题的人似乎是新手C编程。因此,我认为一个现实世界的例子可能会有所帮助。
想象一下你有一个可以容纳六瓶饮料的纸板饮料支架。它一直放在你的车库里,所以除了六瓶饮料,它还包含了车库角落里积攒的各种不良物品:蜘蛛、老鼠窝等等。
计算机缓冲区就像是你分配它后的纸板支架。你不能确定它里面有什么,只知道它有多大。
现在,假设你在支架里放了四瓶饮料。你的支架大小并没有改变,但你现在知道四个空间里有什么。另外两个空间,连同其中可疑的内容,仍然存在。
计算机缓冲区也是一样的。这就是为什么你经常看到一个bufferSize变量来跟踪缓冲区中使用了多少空间。更好的名称可能是numberOfBytesUsedInMyBuffer,但程序员倾向于写得简洁。

2

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

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

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


1

静态持续时间声明的对象(在函数外部声明或使用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);
}

标准明确表示,即使复制了未初始化的temp部分,test也不会引发未定义行为。至少在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]保持相同的值,但并不关心该值是什么,则该函数应如何编写。

1
一般来说,缓冲区不完整并不罕见。通常最好分配比所需更大的缓冲区。(尝试计算精确的缓冲区大小经常是错误的来源,而且往往浪费时间。)
当缓冲区比所需更大时,当缓冲区包含少于其分配大小的数据时,显然重要的是要跟踪有多少数据存在。一般有两种方法来做到这一点:(1)使用显式计数,在单独的变量中保持计数,或者(2)使用“哨兵”值,例如在C语言中标记字符串末尾的 \0 字符。
但接下来的问题是,如果没有全部使用缓冲区,未使用的条目包含什么?
当然,一个答案是,它并不重要。这就是“未使用”的含义。您关心已使用的条目的值,这些值由您的计数或哨兵值记录。您不关心未使用的值。
基本上有四种情况可以预测缓冲区中未使用条目的初始值:
  1. 当您使用具有static持续时间的数组(包括字符数组)进行分配时,所有未使用的条目都将初始化为0。

  2. 当您分配一个数组并给它一个明确的初始化程序时,所有未使用的条目都将初始化为0。

  3. 当您调用calloc时,分配的内存将初始化为全零位。

  4. 当您调用strncpy时,目标字符串将使用\0字符填充到大小为n

在所有其他情况下,缓冲区的未使用部分是不可预测的,并且通常包含上次的内容(无论这意味着什么)。特别地,您无法预测具有自动持续时间(即局部于函数并且没有使用static声明的)的未初始化数组的内容,也无法预测使用malloc获得的内存的内容。 (在这两种情况下,有些时候内存 tend to start out as all-bits-zero第一次,但您绝对不想依赖此功能。)


关于 strncpy 的好处:我很想点赞,因为它教会了用户一个不太知名的副作用,但也想踩一下,因为它隐含地倡导使用这个容易出错的函数,可惜我不能两者兼顾,所以我将不做任何评价。 - chqrlie

1

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


1

我认为正确的答案是你应该始终跟踪写入的字符数。与读取和写入等低级函数一样,需要或提供读取或写入的字符数。同样,std::string在其实现中跟踪字符数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接