为什么MAP_GROWSDOWN映射不增长?

时间:2019-07-04 13:11:16

标签: c linux mmap sigsegv

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

  

MAP_GROWSDOWN

     

此标志用于堆栈。它向内核虚拟内存系统指示映射应向下扩展   记忆。返回地址比存储区域低一页   实际上是在进程的虚拟地址空间中创建的。   触摸映射下方的“防护”页面中的地址将导致   要按页面增长的映射。这种增长可以重复直到   映射增长到下一个较低的高端的页面内   映射,此时触摸“后卫”页面将导致   SIGSEGV信号。

因此,我编写了以下示例来测试映射的增长:

#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而不是扩大映射。在这里生长意味着什么?

3 个答案:

答案 0 :(得分:4)

我知道OP已经接受了答案之一,但是很遗憾,它不能解释为什么MAP_GROWSDOWN有时会起作用。由于此Stack Overflow问题是搜索引擎中的热门搜索之一,因此让我为其他人添加答案。

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

可以重复这种增长,直到映射增长到页面内 下一个较低的映射的高端的位置,此时触摸 “防护”页面将显示SIGSEGV信号。

实际上,内核不允许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;
}

答案 1 :(得分:2)

替换:

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

使用

volatile char *c_ptr_1 = mapped_ptr;

因为:

  

返回地址比该进程的虚拟地址空间中实际创建的内存区域低一页。触摸映射下方的“ guard”页面中的地址将导致映射增长按一页。

请注意,我已经测试了该解决方案,并且可以在内核4.15.0-45-generic上按预期工作。

答案 2 :(得分:1)

首先,您不需要MAP_GROWSDOWN,这也不是主线程堆栈的工作方式。 Analyzing memory mapping of a process with pmap. [stack]什么都没有使用,应该几乎没有使用。手册页中说“用于堆栈”的内容是错误的,应予以修复。

我怀疑这可能是越野车(因为没有人使用它,因此通常没人会在意甚至不知道它是否破裂。)


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

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

然后(当单步执行asm时)maps项实际上更改为较低的起始地址,但结束地址相同,因此当我从400k映射开始时,它实际上是向下增长的。这给出了一个400k初始分配上方的返回地址,该地址在程序运行时增长到404kiB。 (_GROWSDOWN映射的大小是不是增长限制或类似的限制。)

https://bugs.centos.org/view.php?id=4767可能相关; 在CentOS 5.3和5.5的内核版本之间有所变化。和/或与在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进行编译可以使asm一步一步实现。


顺便说一句,关于从glibc中删除该标志的各种谣言显然是错误的。该源代码确实可以编译,并且很显然内核也支持它,而不会默默地忽略它。 (尽管我看到的大小为4096而不是400kiB的行为与默默忽略该标志完全一致。但是gd中仍然存在smaps VmFlag,因此在那个阶段它未被忽略。)< / p>

我检查了一下,它有增长的空间而不会接近另一个映射。因此,IDK为什么当GD映射只有1页时它没有增长。我试了几次,每次都断断续续。有了较大的初始映射,它永远不会出错。

这两次都是存储到mmap返回值(映射的第一页),然后是存储在其下方的4095字节。