为什么 MAP_GROWSDOWN 映射没有增长?

Why is MAP_GROWSDOWN mapping does not grow?

我试图创建 MAP_GROWSDOWN 映射,期望它会自动增长。如手册页中所述:

MAP_GROWSDOWN

This flag is used for stacks. It indicates to the kernel virtual memory system that the mapping should extend downward in memory. The return address is one page lower than the memory area that is actually created in the process's virtual address space. Touching an address in the "guard" page below the mapping will cause the mapping to grow by a page. This growth can be repeated until the mapping grows to within a page of the high end of the next lower mapping, at which point touching the "guard" page will result in a SIGSEGV signal.

所以我写了下面的例子来测试映射增长:

#ifndef _GNU_SOURCE
    #define _GNU_SOURCE
#endif
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <errno.h>
#include <sys/mman.h>
#include <stdio.h>

int main(void){
    char *mapped_ptr = mmap(NULL, 4096,
                            PROT_READ | PROT_WRITE,
                            MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK | MAP_GROWSDOWN,
                            -1, 0);
    if(mapped_ptr == MAP_FAILED){
        int error_code = errno;
        fprintf(stderr, "Cannot do MAP_FIXED mapping."
                        "Error code = %d, details = %s\n", error_code, strerror(error_code));
                        exit(EXIT_FAILURE);
    }
    volatile char *c_ptr_1 = mapped_ptr; //address returned by mmap
    *c_ptr_1 = 'a'; //fine

    volatile char *c_ptr_2 = mapped_ptr - 4095; //1 page below the guard
    *c_ptr_2 = 'b'; //crashes with SEGV
}

所以我得到了 SEGV 而不是增长映射。生长在这里意味着什么?

替换:

volatile char *c_ptr_1 = mapped_ptr - 4096; //1 page below

volatile char *c_ptr_1 = mapped_ptr;

因为:

The return address is one page lower than the memory area that is actually created in the process's virtual address space. Touching an address in the "guard" page below the mapping will cause the mapping to grow by a page.

请注意,我测试了该解决方案,它在内核 4.15.0-45-generic 上按预期工作。

首先,你不想要MAP_GROWSDOWN,这不是主线程堆栈的工作方式。 Analyzing memory mapping of a process with pmap. [stack] 没有人使用它,几乎没有人 应该 使用它。手册页中说 "used for stacks" 的东西是错误的,应该修复。

我怀疑它可能有问题(因为没有人使用它,所以通常没有人关心甚至注意到它是否坏了。)


如果我将 mmap 调用更改为映射超过 1 页,您的代码对我有用。具体来说,我在裸机(Skylake)上尝试了 4096 * 100. 我是 运行 Linux 5.0.1(Arch Linux)。

/proc/PID/smaps 确实显示 gd 标志。

然后(当 single-stepping asm 时)maps 条目实际上更改为较低的起始地址但相同的结束地址,所以当我从 400k 开始时它确实在向下增长映射。这在 return 地址 以上 提供了 400k 的初始分配,当程序运行时它会增长到 404kiB。 (_GROWSDOWN 映射的大小是 而不是 增长限制或类似的东西。)

https://bugs.centos.org/view.php?id=4767 可能是相关的; CentOS 5.3 和 5.5 的内核版本之间发生了一些变化。 And/or 它与在 VM (5.3) 中工作与在裸机 (5.5) 上不增长和故障有关。


我简化了 C 以使用 ptr[-4095] 等:

int main(void){
    volatile char *ptr = mmap(NULL, 4096*100,
                            PROT_READ | PROT_WRITE,
                            MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK | MAP_GROWSDOWN,
                            -1, 0);
    if(ptr == MAP_FAILED){
        int error_code = errno;
        fprintf(stderr, "Cannot do MAP_FIXED mapping."
                        "Error code = %d, details = %s\n", error_code, strerror(error_code));
                        exit(EXIT_FAILURE);
    }

    ptr[0] = 'a';      //address returned by mmap
    ptr[-4095] = 'b';  // grow by 1 page
}

gcc -Og 编译得到 nice-ish 到 single-step 的 asm。


顺便说一句,关于该标志已从 glibc 中删除的各种谣言显然是错误的。这个源代码确实编译了,很明显它也得到了内核的支持,没有被默默地忽略。 (虽然我看到的大小为 4096 而不是 400kiB 的行为与被默默忽略的标志完全一致。但是 gd VmFlag 仍然存在于 smaps 中,所以在那个阶段它没有被忽略。)

我检查了一下,在不接近另一个映射的情况下,它有增长的空间。所以 IDK 为什么当 GD 映射只有 1 页时它没有增长。我试了几次,每次都出现段错误。有了更大的初始映射,它永远不会出错。

两次都是存储到 mmap return 值(正确映射的第一页),然后存储在该值下方的 4095 字节。

我知道 OP 已经接受了其中一个答案,但不幸的是它没有解释为什么 MAP_GROWSDOWN 有时似乎有效。由于这个 Stack Overflow 问题是搜索引擎中最热门的问题之一,让我为其他人添加我的答案。

MAP_GROWSDOWN 的文档需要更新。特别是:

This growth can be repeated until the mapping grows to within a page of the high end of the next lower mapping, at which point touching the "guard" page will result in a SIGSEGV signal.

实际上,内核不允许 MAP_GROWSDOWN 映射与前一映射的距离超过 stack_guard_gap 页。默认值为 256,但可以在内核命令行上覆盖它。由于您的代码没有为映射指定任何所需的地址,因此内核会自动选择一个地址,但很可能会在现有映射末尾的 256 页内结束。

编辑:

此外,v5.0 之前的内核拒绝访问堆栈指针下方超过 64k+256 字节的地址。有关详细信息,请参阅 this kernel commit

即使使用 5.0 之前的内核,该程序也能在 x86 上运行:

#include <sys/mman.h>
#include <stdint.h>
#include <stdio.h>

#define PAGE_SIZE   4096UL
#define GAP     512 * PAGE_SIZE

static void print_maps(void)
{
    FILE *f = fopen("/proc/self/maps", "r");
    if (f) {
        char buf[1024];
        size_t sz;
        while ( (sz = fread(buf, 1, sizeof buf, f)) > 0)
            fwrite(buf, 1, sz, stdout);
        fclose(f);
    }
}

int main()
{
    char *p;
    void *stack_ptr;

    /* Choose an address well below the default process stack. */
    asm volatile ("mov  %%rsp,%[sp]"
        : [sp] "=g" (stack_ptr));
    stack_ptr -= (intptr_t)stack_ptr & (PAGE_SIZE - 1);
    stack_ptr -= GAP;
    printf("Ask for a page at %p\n", stack_ptr);
    p = mmap(stack_ptr, PAGE_SIZE, PROT_READ | PROT_WRITE,
         MAP_PRIVATE | MAP_STACK | MAP_ANONYMOUS | MAP_GROWSDOWN,
         -1, 0);
    printf("Mapped at %p\n", p);
    print_maps();
    getchar();

    /* One page is already mapped: stack pointer does not matter. */
    *p = 'A';
    printf("Set content of that page to \"%s\"\n", p);
    print_maps();
    getchar();

    /* Expand down by one page. */
    asm volatile (
        "mov  %%rsp,%[sp]"  "\n\t"
        "mov  %[ptr],%%rsp" "\n\t"
        "movb $'B',-1(%%rsp)"   "\n\t"
        "mov  %[sp],%%rsp"
        : [sp] "+&g" (stack_ptr)
        : [ptr] "g" (p)
        : "memory");
    printf("Set end of guard page to \"%s\"\n", p - 1);
    print_maps();
    getchar();

    return 0;
}