【问题标题】:Why is MAP_GROWSDOWN mapping does not grow?为什么 MAP_GROWSDOWN 映射不增长?
【发布时间】:2019-11-15 05:48:25
【问题描述】:

我尝试创建 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,而不是增加映射。在这里成长是什么意思?

【问题讨论】:

  • mapped_ptr 已经在保护页面中。在触摸它下面的页面之前先触摸它。
  • @ThomasJager 是的。我试图触摸mapped_ptr。它被罚款了。但是,如果我在此之后触摸mapped_ptr - 4095,它无论如何都会出现段错误(我预计它会进一步增长)。
  • 请添加包含!
  • 嗯,我运行了代码,但无法正常工作。事实上,据我所知,MAP_GROWSDOWN 不再被任何东西使用,也不再用于堆栈,而且它并没有真正起作用,无论如何都应该被删除。它没有足够的保护。
  • @St.Antario 我只是将讨论链接起来,因为 Drepper 说 MAP_GROWSDOWN 无论如何都从根本上被破坏了。它也没有在我的机器上用于任何进程。

标签: c linux mmap segmentation-fault


【解决方案1】:

我知道 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;
}

【讨论】:

    【解决方案2】:

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

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


    如果我将 mmap 调用更改为映射超过 1 页,您的代码对我有效。具体来说,我尝试了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 VmFlag 仍然存在于 smaps 中,因此在那个阶段它不会被忽略。)

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

    两次都存储到 mmap 返回值(映射的第一页正确),然后存储低于 4095 字节。

    【讨论】:

      【解决方案3】:

      替换:

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

      volatile char *c_ptr_1 = mapped_ptr;
      

      因为:

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

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

      【讨论】:

      • 您是否测试过这是否有效?无论如何我无法让它增长,即使我成功读取c_ptr_1[0] 并且它返回 0 并且我可以设置、读取、写入 [-1] will sigsegv。
      • OK 那么内核版本可能很重要! 4.18.0-24-generic x86_64 Ubuntu 在这里。
      • @St.Antario 添加回来。我在没有优化的情况下编译了你的代码,所以volatile 对我来说不是必需的。但它也适用于volatile
      • @MaximEgorushkin 所以在mmapMAP_GROWSDOWN 上只创建了1 个保护页面。我想一旦我们触摸了警卫正下方的页面,就应该保留它。 可以重复这种增长,直到映射增长到下一个较低映射的高端页面内关于重复增长的部分...
      • 但与man page相反,mmap返回值maps中映射的起始地址
      猜你喜欢
      • 2015-04-25
      • 2016-10-25
      • 2023-03-25
      • 2017-11-30
      • 1970-01-01
      • 2011-05-21
      • 2020-12-12
      • 2011-05-16
      • 1970-01-01
      相关资源
      最近更新 更多