我试图了解堆栈在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 ]
答案 0 :(得分:3)
我可以从上面的引号推断出堆栈已映射
从字面上看这仅意味着已分配内存。即从这些虚拟地址到物理页面存在逻辑映射。我们之所以知道这一点,是因为您可以在push
中使用call
或_start
指令,而无需从用户空间进行系统调用来分配堆栈。
实际上,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_GROWSDOWN
,VM_READ
,VM_WRITE
,VM_MAYREAD
,VM_MAYWRITE
和VM_MAYEXEC
。另外,如果将ELF二进制文件指定为具有可执行堆栈(例如,通过使用gcc -z execstack
进行编译),则还将使用VM_EXEC
标志。请注意,在支持向上增长的堆栈的体系结构上,如果内核是使用定义的VM_GROWSUP
编译的,则使用VM_GROWSDOWN
代替CONFIG_STACK_GROWSUP
。在here中可以找到在Linux内核中指定这些标志的代码行。
/proc/.../maps
和pmap
不使用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
的增长空间以防止mmap
地址的随机选择,从创建障碍到堆栈增长。只有在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?的评论(有人发现它起作用,有些人在触摸返回值和下面的页面时发现它仍然存在段错误)听起来像https://bugs.centos.org/view.php?id=4767-MAP_GROWSDOWN
甚至在使用标准主栈VM_GROWSDOWN
映射的方式之外也会出现故障。