使用“推”或“子” x86指令时如何分配堆栈内存?

def*_*tra 2 linux memory memory-management x86-64

我浏览了一段时间,试图例如在执行操作时了解如何将内存分配给堆栈:

push rax
Run Code Online (Sandbox Code Playgroud)

或者移动堆栈指针为子例程的局部变量分配空间:

sub rsp, X    ;Move stack pointer down by X bytes 
Run Code Online (Sandbox Code Playgroud)

据我了解,堆栈段在虚拟内存空间中是匿名的,即不是文件支持的。

我还了解的是,内核不会真正将匿名虚拟内存段映射到物理内存,直到程序对该内存段进行实际操作(即写入数据)为止。因此,尝试在写入该段之前先读取该段可能会导致错误。

在第一个示例中,如果需要,内核将在物理内存中分配一个帧页。在第二个示例中,我假设内核不会在程序实际将数据写入堆栈堆栈段中的地址之前将任何物理内存分配给堆栈段。

我在正确的轨道上吗?

Pet*_*des 5

是的,您在这里的方向正确。 sub rsp, X有点像“惰性”分配:在#PF页面错误异常之后,内核仅在触摸新RSP上方的内存后才执行任何操作,而不仅仅是修改寄存器。但是您仍然可以考虑将内存“分配”,即可以安全使用。

因此,尝试在写入该段之前先读取该段可能会导致错误。

不,读取不会导致错误。从未写入的匿名页面无论是在BSS,堆栈还是中,都将在写时复制映射到一个/物理零页面mmap(MAP_ANONYMOUS)

有趣的事实:在微基准测试中,请确保您触摸了输入数组的内存的每一页,否则,您实际上是在相同的4k或2M物理零页上反复循环,即使您仍然遇到TLB丢失,也会得到L1D缓存命中(以及软页面错误)!gcc会将malloc + memset(0)优化为calloc,但std::vector实际上无论是否要写入所有内存。 memset全局阵列上的数据未优化出来,因此可以正常工作。(或者非零初始化的数组将在数据段中作为文件支持。)


注意,我忽略了映射和有线之间的区别。例如,访问是否会触发软/次页错误以更新页表,还是仅仅是TLB未命中,而硬件页表遍历会找到映射(到零页)。

但是RSP之下的堆栈内存可能根本不会被映射,因此在不先移动RSP的情况下对其进行触摸可能是无效的页面错误,而不是用于解决写时复制的“次要”页面错误。


堆栈内存有一个有趣的变化:堆栈大小限制约为8MB(ulimit -s),但是在Linux中,进程第一个线程的初始堆栈很特殊。例如,我_start在hello-world(动态链接)可执行文件中设置了一个断点,并进行了/proc/<PID>/smaps查看:

7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]
Size:                132 kB
Rss:                   8 kB
Pss:                   8 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         8 kB
Referenced:            8 kB
Anonymous:             8 kB
...
Run Code Online (Sandbox Code Playgroud)

仅8kiB的堆栈已被引用,并由物理页支持。这是预料之中的,因为动态链接器不会使用很多堆栈。

甚至只有132kiB的堆栈被映射到进程的虚拟地址空间。 但是特殊的魔术阻止mmap(NULL, ...)了在堆栈可能增长到的虚拟地址空间的8MiB中随机选择页面。

在当前堆栈映射以下但在堆栈限制内触摸内存 会导致内核增大堆栈映射(在页面错误处理程序中)。

(但只有rsp先进行调整才在其下方128字节rsp,因此ulimit -s unlimited不会使低于1GB的触摸式存储器在rsp此处增加堆栈,但是如果您递减rsp到该位置然后再触摸到存储器则将使堆栈增加。)

这仅适用于初始线程/主线程的堆栈pthreads仅用于mmap(MAP_ANONYMOUS|MAP_STACK)映射无法增长的8MiB块。(MAP_STACK当前为空)。因此,分配后线程堆栈将无法增长(除非MAP_FIXED在其下方有空间,否则必须手动进行操作),并且不受的影响ulimit -s unlimited


这片神奇的防止其他东西从堆栈中的生长区,为不存在选择的地址mmap(MAP_GROWSDOWN),所以千万不能用它来分配新的线程堆栈。(否则,您可能最终会占用新堆栈下方的虚拟地址空间,从而使其无法增长)。只需分配完整的8MiB。另请参见进程虚拟地址空间中其他线程的堆栈在哪里?

MAP_GROWSDOWN确实具有手册页中所述mmap(2)的按需增长功能,但是没有增长限制(除了接近现有映射外),因此(根据手册页)它基于Windows所使用的保护页面,不像主线程的堆栈。

MAP_GROWSDOWN区域底部下方触摸多个页面的内存可能会导致段错误(与Linux的主线程堆栈不同)。针对Linux的编译器不会生成堆栈“探针”来确保在分配大量内存(例如,本地数组或alloca)后按顺序触摸每个4k页面,因此,这MAP_GROWSDOWN是堆栈不安全的另一个原因。

编译器确实会在Windows上发出堆栈探针。

MAP_GROWSDOWN甚至可能根本不起作用,请参阅@BeeOnRope的注释。将其用于任何事物都不是非常安全的,因为如果映射越来越接近其他事物,则可能会发生堆栈冲突安全漏洞。因此,请勿MAP_GROWSDOWN用于任何用途。我我将不去描述Windows使用的保护页面机制,因为很有趣的是Linux的主线程堆栈设计并不是唯一的可能。)

  • 最后,这也回答了您上面遇到的一个问题:只要首先调整了“ rsp”,编译器便会视其为“堆栈增长区域”的任意区域。我能够在当前分配的堆栈(使用`ulimit -s unlimited`)之外写入1 GB,Linux很高兴将堆栈增加到1 GB。这只会起作用,因为主进程堆栈在碰到其他任何东西之前位于VM空间的顶部,大约有10 TB:这不适用于`pthreads'线程,因为这些线程的栈大小固定而未使用`GROWDOWN' `的东西。 (3认同)
  • 彼得·科德斯(Peter Cordes)。我已经对其进行了更深入的研究,答案似乎是“它很复杂,但是文档可能是错误的”。在我的盒子上,在堆栈上分配大量内存并跳入堆栈深处(即,地址要低得多),跳过很多页面都可以。这与我对内核源代码的检查一致。在我的盒子上,“ MAP_GROWNDOWN”根本不起作用:使用[这样的代码](https://unix.stackexchange.com/a/79256/87246)访问映射区域下方时,它总是会出错。这似乎是一个[新错误](https://patchwork.kernel.org/patch/9802797/)。 (2认同)
  • 据我所知,内核中基本上有两个流:到达保护页面的流,最后以`__do_anonymous_page`结尾;而当您跳过保护页面时的流,最后为[这里,在`__do_page_fault中对于x86](https://patchwork.kernel.org/patch/9796395/)。在那里,您可以看到代码通过对“ rsp”的检查来处理“ MAP_GROWSDOWN”情况:因此,由于内核实际上正在检查“ rsp”是否为“”,因此您根本不能将其用作常规的“向下增长”区域靠近”这个区域,否则会发生故障。 (2认同)
  • @BeeOnRope:感谢您所做的所有研究,并与我的回答中的部分评论相关联。 (2认同)