St.*_*rio 6 c linux mmap segmentation-fault
我试图创建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
}
Run Code Online (Sandbox Code Playgroud)
所以我得到了SEGV而不是增加映射。在这里成长是什么意思?
首先,你不想要MAP_GROWSDOWN,而且这也不是主线程堆栈的工作方式。 使用 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 的内核版本之间发生了一些变化。和/或它与在虚拟机 (5.3) 中工作与在裸机 (5.5) 上不增长和故障有关。
我简化了 C 来使用ptr[-4095]etc:
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
}
Run Code Online (Sandbox Code Playgroud)
编译gcc -Og给出的汇编语言对于单步来说是很好的。
顺便说一句,有关该标志已从 glibc 中删除的各种谣言显然是错误的。这个源代码确实可以编译,而且很明显它也受到内核的支持,而不是默默地被忽略。(尽管我看到的大小为 4096 而不是 400kiB 的行为与被默默忽略的标志完全一致。但是 VmFlaggd仍然存在于 中smaps,因此在该阶段不会被忽略。)
我检查了一下,发现它有增长的空间,而不会接近另一个映射。所以我不知道为什么当 GD 映射只有 1 页时它没有增长。我尝试了几次,每次都出现段错误。对于更大的初始映射,它永远不会出错。
两次都是存储到 mmap 返回值(映射正确的第一页),然后存储低于该值的 4095 字节。
我知道 OP 已经接受了其中一个答案,但不幸的是,它并没有解释为什么MAP_GROWSDOWN有时似乎有效。由于这个 Stack Overflow 问题是搜索引擎中最先出现的问题之一,让我为其他人添加我的答案。
MAP_GROWSDOWN需要更新的文档。特别是:
这种增长可以重复,直到映射增长到下一个较低映射的高端页面内,此时触摸“保护”页面将导致 SIGSEGV 信号。
实际上,内核不允许MAP_GROWSDOWN映射stack_guard_gap与前一个映射之间的页面距离更近。默认值为 256,但可以在内核命令行上覆盖它。由于您的代码没有为映射指定任何所需的地址,内核会自动选择一个地址,但很可能会在距现有映射末尾的 256 页内结束。
编辑:
此外,v5.0 之前的内核拒绝访问堆栈指针下方超过 64k+256 字节的地址。有关详细信息,请参阅此内核提交。
即使使用 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;
}
Run Code Online (Sandbox Code Playgroud)
代替:
volatile char *c_ptr_1 = mapped_ptr - 4096; //1 page below
Run Code Online (Sandbox Code Playgroud)
和
volatile char *c_ptr_1 = mapped_ptr;
Run Code Online (Sandbox Code Playgroud)
因为:
返回地址比进程虚拟地址空间中实际创建的内存区域低一页。触摸映射下方的“防护”页面中的地址将导致映射增长一页。
请注意,我测试了该解决方案,它在内核 4.15.0-45-generic 上按预期工作。