为什么此代码中的缓冲区溢出与我的预期不同?

我有这个程序:

#include  #include  #include  void main(void) { char *buffer1 = malloc(sizeof(char)); char *buffer2 = malloc(sizeof(char)); strcpy(buffer2, "AA"); printf("before: buffer1 %s\n", buffer1); printf("before: buffer2 %s\n", buffer2); printf("address, buffer1 %p\n", &buffer1); printf("address, buffer2 %p\n", &buffer2); strcpy(buffer1, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); printf("after: buffer1 %s\n", buffer1); printf("after: buffer2 %s\n", buffer2); } 

哪个印刷品:

 before: buffer1 before: buffer2 AA address, buffer1 0x7ffc700460d8 address, buffer2 0x7ffc700460d0 after: buffer1 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB after: buffer2 B 

我希望这段代码能做什么:

  • 由于char是8位长,我希望两个缓冲区的大小都是1字节/ 8位。

  • 一个ASCII字符长7位,我希望每个缓冲区中有两个字符。

  • 当我直接在一个字节之后分配两个缓冲区时,我希望它们在内存中直接相邻。 因此,我希望每个地址之间的差异是1(因为内存是通过字节?来解决的),而不是我的小程序打印的8。

  • 因为它们在内存中直接相邻,所以当我执行strcpy(buffer1, BBBB);时,我希望缓冲区2被BB溢出strcpy(buffer1, BBBB); 因为第一个BB写入buffer1 ,其余的溢出到buffer2 。 因此,我期望strcpy(buffer1, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); 生产:

    • 缓冲区2中的缓冲区溢出,因此它具有值BBBBBBBBBBBBBBBBBBBBBBBBBBBBB左右。

      • 我如何计算: B的amonut已经strcpy’d – 两个缓冲区的4 B
    • 分段错误。 我只分配了2个字节(因为buffer1buffer2的大小是2个字节)。 由于BBBBBBBBBBBBBBBBBBBBBBBBB既不适合buffer1也不适合buffer2 (因为两者都已填充),因此缓冲区2之后将溢出到下一个内存缓冲区。 而且由于我没有分配,我预计会出现分段错误。

因此,我想问:为什么我的计划与我的期望不同? 我在哪里误解了事情?

我有一个x86_64架构,上面的程序是用gcc version 6.3.1 20170306 (GCC)

我不要求的:

  • 我知道strcpy不是绑定检查,用法是故意的。 我想调查缓冲区溢出等。 因此,请不要写一个答案/评论说我应该使用不同的方法作为strcpy

  • 因为char是8位长,…

这对于所述的体系结构和操作系统是正确的。 (C标准允许char 长度超过8位,但现在这种情况非常罕见;我所知道的唯一例子是TMS320系列DSP,其中char可能是16位。不允许它更小。)

请注意, sizeof(char) == 1 ,因此在代码中编写sizeof(char)foo * sizeof(char)通常被认为是不好的样式。

…我希望两个缓冲区的大小都是1字节/ 8位。

这也是正确的(但见下文)。

  • 一个ASCII字符长7位,我希望每个缓冲区中有两个字符。

由于两个原因,这是不正确的。 首先,没有人再使用7位ASCII。 每个字符实际上是8位长。 其次,两个七位字符适合一个八位缓冲区。 我看到在这个问题的评论中存在一些混淆,所以让我尝试进一步解释:7位可以代表2 7个不同的值,只有足够的空间容纳原始ASCII标准定义的128个不同的字符。 两个七位字符一起可以具有128 * 128 = 16384 = 2 14个不同的值; 这需要14位来表示,并且不适合8位。 你似乎认为它只有2 * 128 = 2 8 ,这将适合8位,但这不对; 这意味着一旦你看到第一个角色,第二个角色只有两种可能性,而不是128。

  • 当我直接在一个字节之后分配两个缓冲区时,我希望它们在内存中直接相邻。 因此,我希望每个地址之间的差异是1(因为内存是通过字节?来解决的),而不是我的小程序打印的8。

正如您自己观察到的那样,您的期望是不正确的。

malloc不需要将连续分配放在一起; 事实上,“这些分配是否彼此相邻”可能不是一个有意义的问题。 C标准不遗余力地避免要求在不指向同一数组的两个指针之间进行任何有意义的比较。

现在,您正在开发一个具有“平面地址空间”的系统,因此比较来自连续分配的指针有意义的(前提是您在自己的大脑中进行,而不是使用代码),并且对于两者之间的差距有合理的解释。分配,但首先我必须指出你打印错误的地址:

 printf("address, buffer1 %p\n", &buffer1); printf("address, buffer2 %p\n", &buffer2); 

这将打印指针变量的地址,而不是缓冲区的地址。 你应该写的

 printf("address, buffer1 %p\n", (void *)buffer1); printf("address, buffer2 %p\n", (void *)buffer2); 

(转换为void *是必需的,因为printf采用可变参数列表。)如果你写过你会看到输出类似于

 address, buffer1 0x55583d9bb010 address, buffer2 0x55583d9bb030 

需要注意的重要一点是,这些分配相差十六个字节,不仅如此,它们都可以被16整除。

malloc需要生成按任何类型的要求对齐的缓冲区,即使您无法将该类型的值放入分配中。 如果地址可以被该数字整除,则地址与某些字节数对齐。 在您的系统上,最大对齐要求为16; 你可以通过运行这个程序来确认这个…

 #include  #include  #include  int main(void) { printf("%zu\n", alignof(max_align_t)); return 0; } 

因此,这意味着malloc返回的所有地址必须可被16整除。因此,当您向malloc请求两个单字节缓冲区时,它们之间必须留下15个字节的间隔。 这并不意味着malloc将尺寸缩小了; C标准专门禁止您访问间隙中的字节。 (我不知道任何现代的商业CPU可以强制执行该禁令,但调试工具如valgrind会,并且已经有实验性的CPU设计可以做到这一点。而且,通常在malloc块之前或之后的空间包含malloc实现内部使用的数据,你不能篡改。)

第二次分配后存在类似的差距。

  • 因为它们在内存中直接相邻,所以当我执行strcpy(buffer1, BBBB);时,我希望缓冲区2被BB溢出strcpy(buffer1, BBBB); 因为第一个BB写入buffer1 ,其余的溢出到buffer2

如前所述,它们在存储器中并不直接相邻,每个B占用8位。 一个B写入您的第一个分配,下一个15分配给两个分配之间的差距,第16个分配到第二个分配,15个之后再分配给第二个分配后的差距,最后一个B和一个NUL到空间超越。

我只分配了2个字节(因为buffer1buffer2的大小是2个字节)。 由于BBBBBBBBBBBBBBBBBBBBBBBBB既不适合buffer1也不适合buffer2 (因为两者都已填充),因此缓冲区2之后将溢出到下一个内存缓冲区。 而且由于我没有分配,我预计会出现分段错误。

我们已经讨论了为什么你的计算不正确,但你确实在第二次分配之后一直写到差距的末尾并进入“超出空间”,那么为什么没有segfault? 这是因为,在操作系统原语级别,内存以称为“ 页面 ”的单位分配给应用程序,这些单元大于您要求的内存量。 CPU只能检测缓冲区溢出,并在超限跨越页边界时触发分段错误。 你刚刚走得不够远。 我在我的计算机上试验了你的程序,它非常相似,我需要在缓冲区1的末尾写入132千字节 (一千字节是1024字节)(有人说这应该叫做kibibyte;它们错了)得到一个段错误。 我的计算机上的页面每个只有4千字节,但malloc要求操作系统提供更大块的内存,因为系统调用很昂贵。

没有得到快速的段错误并不意味着你是安全的; 你很有可能会破坏malloc的内部数据,或者在“超越空间”的某个地方进行另一次分配。 如果我拿到你的原始程序并在最后添加一个free(buffer1)调用,它会在那里崩溃。

首先,请阅读C和C ++中main()返回的内容?


现在关注如何分配内存。

malloc(1)分配了多少内存?

8字节的开销被添加到我们对单个字节的需要,并且总数小于最小值32,所以这是我们的答案:malloc(1)分配32个字节。

这让你的基础变得柔软。

注意: malloc(1)分配32个字节对​​于在该链接上讨论的实现可能是真的,但它非常依赖于实现,并且会有所不同。


另一方面,如果你做了:

 char buffer1[1], buffer2[1]; 

而不是动态分配内存,你会看到不同的结果。 例如,在我的系统中:

 Georgioss-MacBook-Pro:~ gsamaras$ ./a.out // with malloc before: buffer1 before: buffer2 AA address, buffer1 0x7fff5ecb6bd8 address, buffer2 0x7fff5ecb6bd0 after: buffer1 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB after: buffer2 BBBBBBBBBBBBBBBBB Georgioss-MacBook-Pro:~ gsamaras$ gcc -Wall main.c // no malloc Georgioss-MacBook-Pro:~ gsamaras$ ./a.out Abort trap: 6 

提示:规模尚未正式调整; 访问超出请求大小的字节具有未定义的行为 。 (如果它被正式舍入,这将具有实现定义的行为。)

malloc不保证在内存中的位置。 即使使用背靠背调用内存空间也是连续的,您也无法确定。 另外, malloc经常分配比所需更多的空间。 您的代码可能会出现段错误,但不能保证。

带有%s说明符的printf打印指针中的字符,直到遇到NUL (ASCII 0)字符。

请记住,缓冲区溢出是未定义的行为,这意味着:您不确切知道会发生什么。