如果我向一个12字节的缓冲区写入的字节数少于12,会发生什么情况

What happens if I write less than 12 bytes to a 12 byte buffer?

本文关键字:字节数 什么情况 缓冲区 一个 如果 字节      更新时间:2023-10-16

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

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

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

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,有一些编译器标志和内存分配技术可以保证零初始化内存分配,但它是不可移植的。

考虑您的缓冲区,填充了零:

[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(或null字符),但我不确定这是否是一个可能因语言/编译器而异的MS实现。

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

但清洁并不是真正必要的。您需要一个12字节长的缓冲区。您用7个字节填充它。然后你把它传给某个地方——然后你说"这是给你的7个字节"。从缓冲区中读取时,缓冲区的大小是不相关的。你希望其他函数能像你写的一样读取,而不是尽可能多。事实上,在C语言中,通常无法判断缓冲区的长度

附带说明:

可以理解,遍历缓冲区会导致出错(或产生溢出)

没有,这就是问题所在。这就是为什么这是一个巨大的安全问题:没有错误,程序试图继续,所以它有时会执行从未想过的恶意内容。因此,我们不得不在操作系统中添加一系列机制,如ASLR,这将增加程序崩溃的概率,并降低程序继续使用损坏内存的概率。所以,永远不要依赖那些事后想到的警卫,自己观察你的缓冲区边界。

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 
}

这里有一些有趣的细节。

程序知道字符串的长度,因为它以null终止符(值为零的字符)结束。

这就是为什么为了在缓冲区中容纳一个字符串,缓冲区必须比字符串中的字符数至少长1个字符,这样它才能容纳字符串加上null终止符。

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

仅仅因为你还没有使用过这个空间,就认为这个空间是零填充的,这是错误的,在你的程序达到这个地步之前,你不知道这个特定的内存空间是用来做什么的。应将未初始化的内存视为随机且不可靠的内存来处理。

前面的所有答案都非常好,非常详细,但OP似乎是C编程的新手。因此,我认为真实世界示例可能会有所帮助。

想象一下,你有一个可以装六瓶的纸板饮料架。它一直放在你的车库里,所以它不是六个瓶子,而是车库角落里堆积的各种令人讨厌的东西:蜘蛛、老鼠屋等。

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

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

计算机缓冲区也是如此。这就是为什么您经常看到bufferSize变量来跟踪缓冲区的使用量。一个更好的名称可能是numberOfBytesUsedInMyBuffer,但程序员往往简洁得令人抓狂。

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

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

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

通常情况下,缓冲区不足并不罕见。分配比需要的更大的缓冲区通常是一种好的做法

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

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

当然,一个答案是这并不重要。这就是"未使用"的意思。您关心使用的条目的值,这些值由您的计数或sentinel值来说明。您不关心未使用的值。

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

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

  2. 当您分配一个数组并给它一个显式初始值设定项时,所有未使用的项都会初始化为0。

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

  4. 当您调用strncpy时,目标字符串会用字符填充到n大小。

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

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

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

没有指定初始值设定项的静态持续时间声明对象(在函数外部声明的对象,或使用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中未初始化的部分,它也不会调用Undefined Behavior。至少从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);
}

这可能使CCD_ 29和CCD_。对于用于各种目的的高质量编译器是否应在该函数中运行,本标准未作规定。如果程序员要求static1[4]static2[4]保持相同的值,但不在乎该值是多少,则该标准也没有提供关于如何编写函数的指导