如果为了安全起见堆被零初始化,那么为什么堆栈只是未初始化?

如果为了安全起见堆被零初始化,那么为什么堆栈只是未初始化?

在我的 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

你的第二个例子只是暴露了mallocglibc 中实现的一个工件;如果您使用大于 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

你的前提是错误的。

你所描述的“安全”确实是保密,这意味着没有进程可以读取另一个进程的内存,除非该内存在这些进程之间显式共享。在操作系统中,这是隔离并发活动或进程。

操作系统为确保这种隔离所做的事情是,每当进程请求内存进行堆或堆栈分配时,该内存要么来自物理内存中填充有零的区域,要么来自填充了垃圾的区域来自(哪里相同的过程

这可以确保您只看到零或您自己的垃圾,从而确保机密性,并且两个都堆栈是“安全的”,尽管不一定(零)初始化。

您对测量值的解读太多了。

相关内容