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


15

在我的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的以下答案可以解决我的问题。

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


2
不管怎样,当您在OpenBSD上运行时,最新的代码将返回不同的地址和(偶尔的)未初始化的(非零)数据。显然,这并没有说明您在Linux上看到的行为。
Kusalananda

请不要更改问题的范围,也不要尝试对其进行编辑,以使答案和评论多余。在C语言中,“堆”只不过是malloc()和calloc()返回的内存,只有后者将内存清零。newC ++(也称为“堆”)中的运算符在Linux上只是malloc()的包装;内核既不知道也不在乎“堆”是什么。
mosvy

3
您的第二个示例只是在glibc中公开了malloc实现的工件。如果使用大于8个字节的缓冲区重复执行malloc / free,您将清楚地看到只有前8个字节为零。
mosvy

@Kusalananda我明白了。我的结果与OpenBSD的结果不同确实很有趣。显然,您和Mosvy已经证明,我的实验不是像我想的那样发现内核(或链接器)安全协议,而只是发现实现工件。
19th

@thb我相信这可能是正确的观察,是的。
Kusalananda

Answers:


28

malloc()返回的存储初始化为零。永远不要以为是。

在您的测试程序中,这只是fl幸:我想这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中公开实现的工件。如果重复做一次malloc / free用缓冲大于8个字节,会清楚地看到,只有第一个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;
}

输出:

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
好,是的,但这就是为什么我在这里而不是在Stack Overflow上问这个问题的原因。我的问题不是关于C标准,而是关于现代GNU / Linux系统通常链接和加载二进制文件的方式。您的LD_PRELOAD很幽默,但回答了另一个我不想问的问题。
19th

19
我很高兴我让你笑了,但你的假设和偏见一点都不好笑。在“现代GNU / Linux系统”上,二进制文件通常由动态链接器加载,该链接器从动态库运行构造函数,然后从程序进入main()函数。在非常Debian的GNU / Linux 9系统上,即使未使用任何预加载的库,在程序的main()函数之前,malloc()和free()也将被多次调用。
mosvy

23

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

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

这仅说明您从堆栈中获得的结果,请参阅有关常规方法和假设的其他答案。


请注意,在main()调用时间时,初始化例程很可能已修改了返回的内存malloc()-尤其是在链接了C ++库的情况下。假定“堆”被初始化为任何东西,这是一个非常非常糟糕的假设。
Andrew Henle

您的回答与Mosvy一起解决了我的问题。不幸的是,该系统只允许我接受两者之一。否则,我都会接受。
thb

18

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

当操作系统必须将新页面分配给您的进程时(无论是用于其堆栈还是用于的舞台malloc()),它都保证不会暴露其他进程中的数据;确保这一点的通常方法是用零填充(但是用其他任何东西覆盖,甚至包括一个值得页面覆盖的内容,同样有效/dev/urandom-实际上是一些调试malloc()实现会编写非零模式,以捕捉诸如您这样的错误假设)。

如果malloc()可以满足已由该过程使用和释放的内存中的请求,则不会清除其内容(实际上,该清除malloc()与它无关,也不能— 清除必须在将内存映射到您的地址空间)。您可能会获得以前由进程/程序写入的内存(例如之前的main())。

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

如果您真的想了解操作系统级别的情况,建议您绕过C库层,并使用诸如brk()和的系统调用进行交互mmap()


1
一两个星期前,我尝试了不同的实验,要求malloc()free()反复。尽管没有什么要求malloc()重新使用最近释放的相同存储,但是在实验中malloc()确实做到了这一点。碰巧每次都返回相同的地址,但是每次都使内存为空,这是我所料不到的。这对我来说很有趣。进一步的实验导致了今天的问题。
19th

1
@thb,也许我不是足够清晰的-的大多数实现malloc()做绝对没有跟他们交给你的记忆-它或者以前使用的,或新分配的(并因此由OS零)。在测试中,您显然获得了后者。同样,堆栈内存以清除状态提供给您的进程,但您检查的范围还不够深,无法看到您的进程尚未触及的部分。你的堆栈内存之前它给你的进程清除。
Toby Speight

2
@TobySpeight:brk和sbrk被mmap淘汰了。pubs.opengroup.org/onlinepubs/7908799/xsh/brk.html在顶部说“ 遗产 ”。
约书亚

2
如果需要初始化内存,则可以使用calloc(而不是memset
eckes

2
@thb和Toby:有趣的事实:内核中的新页面通常被懒散地分配,并且仅写时复制映射到共享的归零页面。mmap(MAP_ANONYMOUS)除非您也使用,否则会发生这种情况MAP_POPULATE。希望新堆栈页面由新的物理页面作为后盾,并在增长时将其连接(映射在硬件页面表以及内核的指针/长度列表中),因为通常在第一次触摸时会写入新的堆栈内存。但是,是的,内核必须避免以某种方式泄漏数据,并且清零是最便宜,最有用的方法。
彼得·科德斯

9

你的前提是错的。

您所说的“安全性”实际上是机密性,意味着没有进程可以读取另一个进程的内存,除非该内存在这些进程之间显式共享。在操作系统中,这是并发活动或进程隔离的一方面。

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

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

您在测量中读得太多了。


1
现在,问题的“更新”部分明确引用了您的启发性答案。
19th
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.