为什么这段代码中的缓冲区溢出行为与我预期的不同?
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
我希望这段代码做什么:
由于 char 的长度为 8 位,我希望两个缓冲区的大小均为 1 byte/8 位。
一个 ASCII 字符长 7 位,我希望每个缓冲区能容纳两个字符。
因为我直接一个字节分配两个缓冲区,我希望它们在内存中紧挨着彼此。因此,我希望每个地址之间的差异是 1(因为内存是按字节寻址的?),而不是我的小程序打印的 8。
因为它们在内存中紧挨着彼此,所以当我将 strcpy(buffer1, BBBB);
作为第一个 BB
时,我预计缓冲区 2 会被 BB
溢出] 写入 buffer1
,其余溢出到 buffer2
。因此,我希望 strcpy(buffer1, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
产生:
buffer2
中的缓冲区溢出,因此其值为 BBBBBBBBBBBBBBBBBBBBBBBBBBBBB
左右。
- 我是如何计算的:
B
的 amonut 已经被 strcpy'd - 4 B
用于两个缓冲区。
分段错误。我只分配了 2 个字节(因为 buffer1
和 buffer2
的大小加起来是 2 个字节)。由于 BBBBBBBBBBBBBBBBBBBBBBBBB
既不适合 buffer1
也不适合 buffer2
(因为两者都已经填满),因此会溢出到 buffer2
之后的下一个内存缓冲区。因为我还没有分配那个,所以我预计会出现分段错误。
因此,我想问一下:为什么我的程序表现与我的预期不同?我哪里理解错了?
我有一个 x86_64 架构,上面的程序是用 gcc version 6.3.1 20170306 (GCC)
编译的
我不求的:
- 我知道
strcpy
不是绑定检查,是有意使用的。我想调查缓冲区溢出等问题。因此,请不要写 answer/comment 说我应该使用与 strcpy
. 不同的方法
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)
的调用,它会在那里崩溃。
我有这个程序:
#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
我希望这段代码做什么:
由于 char 的长度为 8 位,我希望两个缓冲区的大小均为 1 byte/8 位。
一个 ASCII 字符长 7 位,我希望每个缓冲区能容纳两个字符。
因为我直接一个字节分配两个缓冲区,我希望它们在内存中紧挨着彼此。因此,我希望每个地址之间的差异是 1(因为内存是按字节寻址的?),而不是我的小程序打印的 8。
因为它们在内存中紧挨着彼此,所以当我将
strcpy(buffer1, BBBB);
作为第一个BB
时,我预计缓冲区 2 会被BB
溢出] 写入buffer1
,其余溢出到buffer2
。因此,我希望strcpy(buffer1, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
产生:buffer2
中的缓冲区溢出,因此其值为BBBBBBBBBBBBBBBBBBBBBBBBBBBBB
左右。- 我是如何计算的:
B
的 amonut 已经被 strcpy'd - 4B
用于两个缓冲区。
- 我是如何计算的:
分段错误。我只分配了 2 个字节(因为
buffer1
和buffer2
的大小加起来是 2 个字节)。由于BBBBBBBBBBBBBBBBBBBBBBBBB
既不适合buffer1
也不适合buffer2
(因为两者都已经填满),因此会溢出到buffer2
之后的下一个内存缓冲区。因为我还没有分配那个,所以我预计会出现分段错误。
因此,我想问一下:为什么我的程序表现与我的预期不同?我哪里理解错了?
我有一个 x86_64 架构,上面的程序是用 gcc version 6.3.1 20170306 (GCC)
我不求的:
- 我知道
strcpy
不是绑定检查,是有意使用的。我想调查缓冲区溢出等问题。因此,请不要写 answer/comment 说我应该使用与strcpy
. 不同的方法
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 dostrcpy(buffer1, BBBB);
as the firstBB
are written tobuffer1
and the rest overflows tobuffer2
.
如前所述,它们在内存中并不直接相邻,每个B占用8位。一个 B 写入您的第一个分配,接下来的 15 个写入两次分配之间的间隙,第 16 个写入第二个分配,之后还有 15 个写入间隙 after 第二个分配,并且最后1个B和1个NUL到space beyond.
I have only allocated 2 bytes (since the size of
buffer1
andbuffer2
are together 2 bytes). SinceBBBBBBBBBBBBBBBBBBBBBBBBB
doesn't fit into neitherbuffer1
norbuffer2
(because both are already filled), that would be overflown to the next memory buffer afterbuffer2
. 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)
的调用,它会在那里崩溃。