在我的 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 的以下答案共同解决了我的问题。
另请参阅堆栈溢出:为什么gcc中malloc将值初始化为0?(信用:@bta)。
答案1
malloc() 返回的存储空间为不是零初始化。永远不要假设它是。
在你的测试程序中,这只是一个侥幸:我猜malloc()
刚刚得到了一个新的块mmap()
,但也不要依赖于此。
举个例子,如果我在我的机器上运行你的程序:
$ echo 'void __attribute__((constructor)) p(void){
void *b = malloc(4444); memset(b, 4, 4444); free(b);
}' | cc -include stdlib.h -include string.h -xc - -shared -o pollute.so
$ LD_PRELOAD=./pollute.so ./your_program
a at 0x7ffd40d3aa60: 1256994848 21891 1256994464 21891 1087613792 32765 0 0
b at 0x55834c75d010: 67372036 67372036 67372036 67372036 67372036 67372036 67372036 67372036
你的第二个例子只是暴露了malloc
glibc 中实现的一个工件;如果您使用大于 8 字节的缓冲区重复malloc
/ ,您将清楚地看到只有前 8 个字节被归零,如以下示例代码所示。free
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 4;
const size_t m = 0x10;
int main()
{
for (size_t i = n; i; --i) {
int *const p = malloc(m*sizeof(int));
printf("%p ", p);
for (size_t j = 0; j < m; ++j) {
printf("%d:", p[j]);
++p[j];
printf("%d ", p[j]);
}
free(p);
printf("\n");
}
return 0;
}
输出:
0x55be12864010 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1
0x55be12864010 0:1 0:1 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2
0x55be12864010 0:1 0:1 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3
0x55be12864010 0:1 0:1 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4
答案2
无论堆栈如何初始化,您都不会看到原始堆栈,因为 C 库在调用之前会执行许多操作main
,并且它们会接触堆栈。
使用 GNU C 库,在 x86-64 上,执行从_开始入口点,调用__libc_start_main
进行设置,后者最终调用main
.但在调用之前main
,它会调用许多其他函数,这会导致各种数据被写入堆栈。堆栈的内容在函数调用之间不会被清除,因此当您进入 时main
,您的堆栈包含先前函数调用的剩余内容。
这仅解释了您从堆栈中获得的结果,请参阅有关您的一般方法和假设的其他答案。
答案3
在这两种情况下,你都会得到未初始化的内存,你不能对其内容做出任何假设。
当操作系统必须向您的进程分配新页面时(无论是用于其堆栈还是用于 所使用的区域malloc()
),它保证不会暴露来自其他进程的数据;确保这一点的通常方法是用零填充它(但用其他任何东西覆盖也同样有效,甚至包括一页的内容/dev/urandom
- 事实上,一些调试malloc()
实现会写入非零模式,以捕获像您这样的错误假设)。
如果malloc()
可以满足该进程已使用和释放的内存的请求,则其内容不会被清除(事实上,清除与此无关,malloc()
而且不可能 - 它必须在内存映射到之前发生)您的地址空间)。您可能会获得以前由您的进程/程序写入的内存(例如 before main()
)。
在您的示例程序中,您会看到一个malloc()
尚未被此进程写入的区域(即它直接来自新页面)和一个已写入的堆栈(通过main()
程序中的预编码)。如果您检查更多堆栈,您会发现它向下填充了零(沿其增长方向)。
如果您确实想了解操作系统级别发生的情况,我建议您绕过 C 库层并使用系统调用(例如brk()
和)进行交互mmap()
。
答案4
你的前提是错误的。
你所描述的“安全”确实是保密,这意味着没有进程可以读取另一个进程的内存,除非该内存在这些进程之间显式共享。在操作系统中,这是隔离并发活动或进程。
操作系统为确保这种隔离所做的事情是,每当进程请求内存进行堆或堆栈分配时,该内存要么来自物理内存中填充有零的区域,要么来自填充了垃圾的区域来自(哪里相同的过程。
这可以确保您只看到零或您自己的垃圾,从而确保机密性,并且两个都堆和堆栈是“安全的”,尽管不一定(零)初始化。
您对测量值的解读太多了。