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

Why does the buffer overflow in this code behave different from what I expect?

我有这个程序:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

我希望这段代码做什么:

因此,我想问一下:为什么我的程序表现与我的预期不同?我哪里理解错了?

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

编译的

我不求的:

malloc 不保证在内存中的位置。即使背靠背调用,您也无法确定内存 space 是否是连续的。此外,malloc 分配的 space 经常比必要的多。您的代码很可能会发生段错误,但不能保证。

带有 %s 说明符的

printf 从指针打印字符,直到遇到 NUL (ASCII 0) 字符。

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

首先请阅读What should main() return in C and C++?


现在关注如何分配内存。

How much memory does malloc(1) allocate?

8 bytes of overhead are added to our need for a single byte, and the total is smaller than the minimum of 32, so that's our answer: malloc(1) allocates 32 bytes.

这让你的基础变软了。

注意:malloc(1) 分配 32 个字节这对于 link 上讨论的实现可能是正确的,但它非常依赖于实现并且会有所不同。


另一方面,如果您这样做了:

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

提示:尺寸尚未正式四舍五入;访问超出请求大小的字节具有 未定义的行为 。 (如果它被正式汇总,这将具有实现定义的行为。)

  • As a char is 8 bits long, ...

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

请注意 sizeof(char) == 1 根据定义 因此通常认为在代码中编写 sizeof(char)foo * sizeof(char) 是不好的风格。

... i expect that both buffers have the size of 1 byte/8 bits.

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

  • One ASCII char is 7 bits long, i expect that two characters fit into each buffer.

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

  • As I allocate two buffers of one byte directly after each other, i expect that they are directly next to each other in the memory. Therefore, i expect that the difference between each address is 1 (since the memory is addressed by byte?), and not 8 as my little program has printed.

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

malloc 不需要将连续的分配彼此相邻;事实上,"are these allocations next to each other" 可能不是一个有意义的问题。 C 标准不遗余力地 避免 要求在不指向同一数组的两个指针之间进行 any 有意义的比较。

现在,您正在使用 "flat address space" 的系统工作,因此 比较来自连续分配的指针是有意义的(前提是您在自己的大脑中这样做) ,而不是代码)并且对于分配之间的差距有一个合乎逻辑的解释,但首先我必须指出你打印了错误的地址:

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

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

malloc 需要根据 any 类型生成 aligned 的缓冲区,即使你不能将该类型的值放入分配中。如果地址可以被该数字整除,则该地址与某个字节数对齐。在您的系统上,最大对齐要求是 16;您可以通过 运行 这个程序来确认这一点...

#include <stdalign.h>
#include <stddef.h>
#include <stdio.h>
int main(void) { printf("%zu\n", alignof(max_align_t)); return 0; }

所以这意味着malloc返回的所有地址必须能被16整除。因此,当你向malloc请求两个1字节的缓冲区时,它必须留出15字节的间隙它们之间。这 并不 意味着 malloc 向上取整; C 标准明确禁止您访问间隙中的字节。 (我不知道有任何现代商业 CPU 可以强制执行该禁令,但像 valgrind 这样的调试工具可以,并且已经有实验性 CPU 设计可以做到这一点。此外,malloc 块之前或之后的 space 通常包含 malloc 实现内部使用的数据,您不得篡改这些数据。)

第二次分配后也有类似的差距。

  • As they are directly next to each other in the memory, i expect buffer 2 to be overflown with BB when I do strcpy(buffer1, BBBB); as the first BB are written to buffer1 and the rest overflows to buffer2.

如前所述,它们在内存中并不直接相邻,每个B占用8位。一个 B 写入您的第一个分配,接下来的 15 个写入两次分配之间的间隙,第 16 个写入第二个分配,之后还有 15 个写入间隙 after 第二个分配,并且最后1个B和1个NUL到space beyond.

I have only allocated 2 bytes (since the size of buffer1 and buffer2 are together 2 bytes). Since BBBBBBBBBBBBBBBBBBBBBBBBB doesn't fit into neither buffer1 nor buffer2 (because both are already filled), that would be overflown to the next memory buffer after buffer2. And because i have not allocated that, i'd expect an segmentation fault.

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

没有收到提示段错误并不意味着您是安全的;您很有可能破坏了 malloc 的内部数据,或者破坏了 "space beyond" 中的其他分配。如果我采用您的原始程序并在末尾添加对 free(buffer1) 的调用,它会在那里崩溃。