在我的 Debian GNU/Linux 9 系统上,执行二进制文件时,
- 堆栈未初始化,但
- 堆是零初始化的。
为什么?
我假设零初始化可以提高安全性,但是,如果对于堆,那么为什么不也用于堆栈呢?堆栈也不需要安全性吗?
据我所知,我的问题并不特定于 Debian。
示例 C 代码:
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 8;
// --------------------------------------------------------------------
// UNINTERESTING CODE
// --------------------------------------------------------------------
static void print_array(
const int *const p, const size_t size, const char *const name
)
{
printf("%s at %p: ", name, p);
for (size_t i = 0; i < size; ++i) printf("%d ", p[i]);
printf("\n");
}
// --------------------------------------------------------------------
// INTERESTING CODE
// --------------------------------------------------------------------
int main()
{
int a[n];
int *const b = malloc(n*sizeof(int));
print_array(a, n, "a");
print_array(b, n, "b");
free(b);
return 0;
}
输出:
a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0
当然,C 标准不要求malloc()
在分配内存之前清除内存,但我的 C 程序只是为了说明。这个问题不是关于 C 或 C 标准库的问题。相反,问题是关于为什么内核和/或运行时加载程序将堆归零而不是堆栈归零的问题。
另一个实验
我的问题是关于可观察到的 GNU/Linux 行为而不是标准文档的要求。如果不确定我的意思,那么试试这段代码,它会调用进一步的未定义行为(未定义,即就 C 标准而言)来说明这一点:
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 4;
int main()
{
for (size_t i = n; i; --i) {
int *const p = malloc(sizeof(int));
printf("%p %d ", p, *p);
++*p;
printf("%d\n", *p);
free(p);
}
return 0;
}
我机器的输出:
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
就 C 标准而言,行为是未定义的,所以我的问题与 C 标准无关。调用malloc()
不需要每次都返回相同的地址,但是由于每次调用malloc()
确实碰巧返回相同的地址,有趣的是,堆上的内存每次都归零。
相比之下,堆栈似乎并没有归零。
我不知道后面的代码会在你的机器上做什么,因为我不知道 GNU/Linux 系统的哪一层导致了观察到的行为。你可以尝试一下。
更新
@Kusalananda 在评论中观察到:
对于它的价值,您最近的代码在 OpenBSD 上运行时会返回不同的地址和(偶尔)未初始化(非零)数据。这显然没有说明您在 Linux 上看到的行为。
我的结果与 OpenBSD 上的结果不同确实很有趣。显然,我的实验发现的不是内核(或链接器)安全协议,正如我所想的那样,而仅仅是一个实现工件。
有鉴于此,我相信@mosvy、@StephenKitt 和@AndreasGrapentin 下面的答案一起解决了我的问题。
另请参阅 Stack Overflow:为什么 malloc 将 gcc 中的值初始化为 0?(信用:@bta)。
malloc() 返回的存储不是零初始化的。永远不要假设它是。
在您的测试程序中,这只是一个侥幸:我想
malloc()
刚刚获得了一个新的块mmap()
,但也不要依赖它。例如,如果我以这种方式在我的机器上运行您的程序:
您的第二个示例只是
malloc
在 glibc 中公开实现的工件;如果您使用大于 8 字节的缓冲区重复malloc
/ ,您将清楚地看到只有前 8 个字节被清零,如以下示例代码所示。free
输出:
无论堆栈是如何初始化的,您都看不到原始堆栈,因为 C 库在调用 之前做了很多事情
main
,并且它们接触了堆栈。使用 GNU C 库,在 x86-64 上,执行从_start入口点开始,该入口点调用
__libc_start_main
设置,而后者最终调用main
. 但在调用 之前main
,它会调用许多其他函数,这会导致将各种数据写入堆栈。堆栈的内容不会在函数调用之间清除,因此当您进入 时main
,您的堆栈包含先前函数调用的剩余部分。这仅解释了您从堆栈中获得的结果,请参阅有关您的一般方法和假设的其他答案。
在这两种情况下,您都会获得未初始化的内存,并且您无法对其内容做出任何假设。
当操作系统必须为您的进程分配一个新页面时(无论是用于它的堆栈还是用于 使用的竞技场
malloc()
),它保证它不会暴露来自其他进程的数据;确保它的常用方法是用零填充它(但用其他任何东西覆盖同样有效,甚至包括一页的价值/dev/urandom
- 事实上,一些调试malloc()
实现编写非零模式,以捕捉错误的假设,如你的假设)。如果
malloc()
能满足本进程已经使用和释放的内存的请求,则其内容不会被清除(实际上,清除无关malloc()
也不能——必须在内存映射到内存之前发生)您的地址空间)。您可能会获得以前由您的进程/程序写入的内存(例如 beforemain()
)。在您的示例程序中,您会看到
malloc()
该进程尚未写入的区域(即,它直接来自新页面)和已写入的堆栈(通过main()
程序中的预代码)。如果您检查更多的堆栈,您会发现它在更下方(在其增长方向上)是零填充的。如果您真的想了解在操作系统级别发生了什么,我建议您绕过 C 库层并使用系统调用(例如 and )进行
brk()
交互mmap()
。你的前提是错误的。
您所描述的“安全性”实际上是机密性,这意味着没有进程可以读取另一个进程的内存,除非这些内存在这些进程之间明确共享。在操作系统中,这是并发活动或进程隔离的一个方面。
操作系统为确保这种隔离所做的工作是,每当进程为堆或堆栈分配请求内存时,该内存要么来自物理内存中的一个区域,该区域被零填充,要么被垃圾填充来自同一个过程。
这确保您只看到零或您自己的垃圾,因此确保机密性,并且堆和堆栈都是“安全的”,尽管不一定(零)初始化。
您对测量结果的解读过多。