【问题标题】:Analyzing memory mapping of a process with pmap. [stack]使用 pmap 分析进程的内存映射。 [堆]
【发布时间】:2019-11-15 12:21:48
【问题描述】:

我正在尝试了解堆栈在 Linux 中的工作原理。我阅读了关于堆栈和进程初始化的AMD64 ABI 部分,但不清楚应该如何映射堆栈。这是相关的引用(3.4.1):

堆栈状态

本节描述了exec (BA_OS) 为其创建的机器状态 新流程。

未指定数据段和堆栈段最初是否为 是否映射了执行权限。需要的应用 在堆栈或数据段上执行代码应该采取适当的 预防措施,例如,致电mprotect()

所以我可以从上面的引号中推断出堆栈是映射的(如果PROT_EXEC 用于创建映射,则未指定)。映射也是由exec 创建的。

问题是“主线程”的堆栈是使用MAP_GROWSDOWN | MAP_STACK 映射还是使用sbrk

查看pmap -x <pid>堆栈被[stack]标记为

00007ffc04c78000     132      12      12 rw---   [ stack ]

创建映射

mmap(NULL, 4096,
     PROT_READ | PROT_WRITE,
     MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK,
     -1, 0);

只需创建匿名映射,如 pmap -x <pid> 所示

00007fb6e42fa000       4       0       0 rw---   [ anon ]

【问题讨论】:

  • 函数:sbrk()用于改变数据段大小。和栈无关

标签: c linux linux-kernel stack mmap


【解决方案1】:

我可以从上面的引号中推断出堆栈是映射的

字面意思只是分配了内存。即存在从这些虚拟地址到物理页面的逻辑映射。我们知道这一点,因为您可以在 _start 中使用 pushcall 指令,而无需从用户空间进行系统调用来分配堆栈。

事实上,x86-64 System V ABI 指定 argc、argv 和 envp 在进程启动时位于堆栈中。

问题是“主线程”的堆栈是使用MAP_GROWSDOWN | MAP_STACK 映射还是使用sbrk

ELF 二进制加载程序为主线程的堆栈设置_GROWSDOWN 标志,但不设置MAP_STACK 标志。这是内核内部的代码,它不通过常规的mmap系统调用接口。

用户空间中没有任何东西使用mmap(MAP_GROWSDOWN),所以通常主线程堆栈是内核中唯一具有VM_GROWSDOWN标志的映射。)

用于堆栈的虚拟内存区域 (VMA) 的标志的内部名称称为VM_GROWSDOWN。如果您有兴趣,这里是用于主线程堆栈的所有标志:VM_GROWSDOWNVM_READVM_WRITEVM_MAYREADVM_MAYWRITEVM_MAYEXEC。此外,如果指定 ELF 二进制文件具有可执行堆栈(例如,通过使用 gcc -z execstack 编译),则还使用 VM_EXEC 标志。请注意,在支持向上增长的堆栈的架构上,如果内核编译时定义了CONFIG_STACK_GROWSUP,则使用VM_GROWSUP 而不是VM_GROWSDOWN。在 Linux 内核中指定这些标志的代码行可以找到 here

/proc/.../mapspmap 不使用 VM_GROWSDOWN - 它们依赖于地址比较。因此,他们可能无法准确确定主线程堆栈占用的虚拟地址空间的确切范围(参见example)。另一方面,/proc/.../smaps 查找VM_GROWSDOWN 标志并将具有此标志的每个内存区域标记为gd。 (虽然它似乎忽略了VM_GROWSUP。)

所有这些工具/文件都会忽略MAP_STACK 标志。事实上,整个 Linux 内核都忽略了这个标志(这可能是程序加载器不设置它的原因。)用户空间只将它传递给未来验证,以防内核确实想要启动专门处理线程堆栈分配。


sbrk 在这里毫无意义;堆栈与“中断”不连续,brk 堆无论如何都会向上堆栈增长。 Linux 将堆栈置于非常接近虚拟地址空间的顶部。所以当然不能用sbrk(在内核中的等价物)分配主堆栈。


不,没有任何东西使用MAP_GROWSDOWN,甚至没有辅助线程堆栈,因为它通常不能安全使用。

mmap(2) 手册页上写着 MAP_GROWSDOWN 是“用于堆栈”,这可笑地过时了,而且具有误导性。见How to mmap the stack for the clone() system call on linux?。作为 Ulrich Drepper explained in 2008,使用 MAP_GROWSDOWN 的代码通常会被破坏,并建议从 Linux mmap 和 glibc 标头中删除标志。 (这显然没有发生,但 pthreads 在那之前就没有使用过它,如果有的话。)


MAP_GROWSDOWN 为内核内部的映射设置VM_GROWSDOWN 标志。主线程使用该标志来启用增长机制,因此线程堆栈可能能够以与主堆栈相同的方式增长:任意远(最多 ulimit -s?)如果堆栈指针位于缺页位置下方。 (Linux 不需要“堆栈探测器”来接触大型多页堆栈数组或alloca 的每一页。)

(线程堆栈在前面完全分配;只有物理页面的正常延迟分配才能支持该虚拟分配,以避免浪费线程堆栈空间。)

MAP_GROWSDOWN 映射也可以按照mmap 手册页描述的方式增长:访问最低映射页面下方的“保护页面”也会触发增长,即使它低于红色区域的底部。

但是主线程的堆栈有你mmap(MAP_GROWSDOWN)获得的魔力。它保留了高达ulimit -s的增长空间,以防止随机选择@ 987654380@ 地址从创建障碍到堆栈增长。这种魔法仅适用于内核程序加载器,它在execve() 期间映射主线程的堆栈,使其免受mmap(NULL, ...) 随机阻塞未来堆栈增长的影响。

mmap(MAP_FIXED) 仍然可以为主堆栈创建障碍,但如果您使用MAP_FIXED,您将 100% 负责不破坏任何东西。 (Unlimited stack cannot grow beyond the initial 132KiB if MAP_FIXED involved?)。 MAP_FIXED 将替换现有的映射和​​保留,但其他任何东西都会将主线程的堆栈增长空间视为保留;。 (我认为这是真的;值得尝试使用 MAP_FIXED_NOREPLACE 或只是一个非 NULL 提示地址)

pthread_create 不会将MAP_GROWSDOWN 用于线程堆栈,其他任何人也不应该使用。一般不使用。 Linux pthreads 默认为线程堆栈分配完整大小。这会消耗虚拟地址空间,但(直到实际触及)不消耗物理页面。

Why is MAP_GROWSDOWN mapping does not grow? 上 cmets 的不一致结果(有些人发现它有效,有些人发现它在触摸返回值和下面的页面时仍然存在段错误)听起来像 https://bugs.centos.org/view.php?id=4767 - MAP_GROWSDOWN 甚至可能在使用标准主堆栈VM_GROWSDOWN 映射的方式。

【讨论】:

  • 如果我们检查pmap的源代码,我们可以看到它relies来自/proc/pid/stat文件的信息来确定给定地址是否在堆栈中.具有此信息的proc_t 类型的对象被初始化here。其中一个字段是start_stack,它保存着程序栈的基地址(主线程的栈)。 pmap 还有uses...
  • .../proc/pid/smap 获取堆栈的大小。然后在this 代码行,它将给定地址与基堆栈地址进行比较,如果它落入堆栈区域,则将其标记为[ stack ]。因此,是否使用MAP_GROWSDOWN 标志分配内存区域并不重要。该工具只是不检查此标志。此外,当将MAP_GROWSDOWN 标志传递给mmap 时,它会被转换为一个名为VM_GROWSDOWN 的内部标志...
  • 页面错误处理程序检查此标志以确定堆栈中是否发生页面错误并且它应该增加堆栈(如果可能)。所有线程的堆栈都标有VM_GROWSDOWN。我认为 pmap 使用 MAP_GROWSDOWN 而不是与程序堆栈地址进行比较,它会为分配有此标志的任何区域打印 [ stack ],即使该区域不用作线程的堆栈。
  • 所以mmap(2) 手册页说MAP_GROWSDOWN“用于堆栈”是准确的,因为它确实如此。
  • @HadiBrais:刚刚完成对我答案的更新。感谢您的编辑,关于内核 VM 标志的讨论很有用。我回顾了一个事实(?),用户空间中的任何内容通常都不会使用MAP_GROWSDOWN,因为我认为这是很重要的一点。我没有寻找任何进一步的讨论,但很明显MAP_GROWSDOWN 实际上并没有从 glibc 标头中删除。
猜你喜欢
  • 2014-10-09
  • 2015-04-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-28
  • 1970-01-01
相关资源
最近更新 更多