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

thb*_*thb 17 linux security memory

在我的 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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713 
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0 
Run Code Online (Sandbox Code Playgroud)

当然,C 标准malloc()在分配内存之前并不要求清除内存,但我的 C 程序仅用于说明。问题不是关于 C 或关于 C 的标准库的问题。相反,问题是关于为什么内核和/或运行时加载器将堆清零而不是堆栈清零的问题。

另一个实验

我的问题是关于可观察的 GNU/Linux 行为,而不是标准文档的要求。如果不确定我的意思,请尝试以下代码,它会调用进一步的未定义行为(未定义,即就 C 标准而言)来说明这一点:

a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713 
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0 
Run Code Online (Sandbox Code Playgroud)

我机器的输出:

0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
Run Code Online (Sandbox Code Playgroud)

就 C 标准而言,行为是未定义的,所以我的问题与 C 标准无关。调用malloc()不需要每次都返回相同的地址,但是由于这个调用malloc()确实每次都返回相同的地址,有趣的是注意到堆上的内存每次都被清零。

相比之下,堆栈似乎没有归零。

我不知道后面的代码会在你的机器上做什么,因为我不知道 GNU/Linux 系统的哪一层导致了观察到的行为。你可以试试。

更新

@Kusalananda 在评论中观察到:

值得一提的是,您的最新代码在 OpenBSD 上运行时会返回不同的地址和(偶尔)未初始化(非零)数据。这显然没有说明您在 Linux 上看到的行为。

我的结果与 OpenBSD 上的结果不同确实很有趣。显然,我的实验没有发现内核(或链接器)安全协议,正如我所想的那样,而只是一个实现工件。

有鉴于此,我相信@mosvy、@StephenKitt 和@AndreasGrapentin 的以下答案共同解决了我的问题。

另请参阅堆栈溢出:为什么 malloc 将 gcc 中的值初始化为 0?(信用:@bta)。

mos*_*svy 30

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
Run Code Online (Sandbox Code Playgroud)

你的第二个例子只是malloc在 glibc 中暴露了一个实现的工件;如果您使用大于 8 个字节的缓冲区重复malloc/ free,您将清楚地看到只有前 8 个字节被清零,如下面的示例代码所示。

#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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

  • 我很高兴让你发笑,但你的假设和偏见一点也不好笑。在“现代 GNU/Linux 系统”上,二进制文件通常由动态链接器加载,该链接器在从程序获取 main() 函数之前运行动态库中的构造函数。在您的 Debian GNU/Linux 9 系统上, malloc() 和 free() 将在您的程序的 main() 函数之前被调用不止一次,即使没有使用任何预加载的库也是如此。 (19认同)
  • 嗯,是的,但这就是我在这里而不是在 Stack Overflow 上问这个问题的原因。我的问题不是关于 C 标准,而是关于现代 GNU/Linux 系统通常链接和加载二进制文件的方式。你的 LD_PRELOAD 很幽默,但回答了另一个问题,而不是我本来想问的问题。 (2认同)

Ste*_*itt 25

无论堆栈如何初始化,您都看不到原始堆栈,因为 C 库在调用 之前做了很多事情main,并且它们会接触堆栈。

对于 GNU C 库,在 x86-64 上,执行从_start入口点开始,该入口点调用__libc_start_main进行设置,而后者最终调用main. 但是在调用 之前main,它会调用许多其他函数,这会导致将各种数据写入堆栈。堆栈的内容不会在函数调用之间清除,因此当您进入 时main,您的堆栈包含先前函数调用的剩余部分。

这仅解释了您从堆栈中获得的结果,请参阅有关您的一般方法和假设的其他答案。


Tob*_*ght 20

在这两种情况下,您都会获得未初始化的内存,并且无法对其内容做出任何假设。

当操作系统必须将新页面分配给您的进程时(无论是用于其堆栈还是用于 所使用的领域malloc()),它保证不会暴露来自其他进程的数据;确保这一点的常用方法是用零填充它(但用其他任何东西覆盖它同样有效,甚至包括一页/dev/urandom——事实上,一些调试malloc()实现编写了非零模式,以捕捉错误的假设,比如你的假设)。

如果malloc()可以满足这个进程已经使用和释放的内存的请求,它的内容就不会被清除(实际上,清除是无关紧要的,malloc()也不能——它必须在内存映射到之前发生您的地址空间)。您可能会获得先前由您的进程/程序写入的内存(例如 before main())。

在您的示例程序中,您会看到malloc()该进程尚未写入的区域(即它直接来自新页面)和已写入的堆栈(通过main()程序中的预代码)。如果你检查更多的堆栈,你会发现它在更远的地方(在它的增长方向上)是零填充的。

如果你真的想了解情况,在操作系统层面上,我建议你绕过C库层和交互使用系统调用,如brk()mmap()代替。

  • @TobySpeight:brk 和 sbrk 已被 mmap 淘汰。http://pubs.opengroup.org/onlinepubs/7908799/xsh/brk.html 在顶部说 LEGACY。 (2认同)
  • 如果您需要使用 `calloc` 初始化内存可能是一个选项(而不是 `memset`) (2认同)
  • @thb 和 Toby:有趣的事实:内核中的新页面通常被延迟分配,并且仅将写时复制映射到共享的归零页面。这种情况发生在 `mmap(MAP_ANONYMOUS)` 上,除非你也使用 `MAP_POPULATE`。新的堆栈页面希望由新的物理页面支持,并在增长时连接(映射到硬件页面表,以及内核的指针/映射长度列表),因为通常新堆栈内存在第一次接触时被写入. 但是是的,内核必须以某种方式避免泄漏数据,而归零是最便宜和最有用的。 (2认同)

小智 9

你的前提是错误的。

您所描述的“安全性”实际上是机密性,这意味着任何进程都不能读取另一个进程的内存,除非这些内存在这些进程之间明确共享。在操作系统中,这是隔离并发活动或进程的一个方面。

操作系统为确保这种隔离所做的工作是,每当进程请求内存用于堆或堆栈分配时,该内存要么来自物理内存中被零填充的区域,要么来自充满垃圾的区域来自同一个过程

这将确保你只看到过零,或者你自己的垃圾,所以保密性得到保证,并且栈是“安全”的,虽然不一定(零)初始化。

你对你的测量读得太多了。