当使用'push'或'sub'x86指令时如何分配堆栈内存?

我一直在浏览一段时间,我想了解如何将内存分配给堆栈时,例如:

push rax 

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

 sub rsp, X ;Move stack pointer down by X bytes 

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

我也明白,内核实际上不会将匿名的虚拟内存段映射到物理内存,直到程序实际上对内存段做某事,即写入数据。 所以,在写入之前尝试读取该段可能会导致错误。

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

我在这里的正确轨道?

我在这里的正确轨道?

是的,非常接近。

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

不,阅读不会导致错误。 从未写入的匿名页面是写入时复制映射到物理零页面的,无论它们处于BSS,堆栈还是mmap(MAP_ANONYMOUS)

有趣的事实:在微基准测试中,确保你触摸输入数组的每一页内存,否则你实际上在相同的物理4k或2M零页重复循环,即使你仍然得到TLB失败,也会得到L1D缓存命中(和软页面错误)! gcc会优化malloc + memset(0)到calloc ,但std::vector实际上会写所有的内存,不管你是否喜欢它。 全局数组上的memset没有被优化出来,所以工作。 (或者非零初始化数组将在数据段中被文件支持。)


请注意,我遗漏了映射与有线之间的区别。 即访问是否触发软页面错误来更新页表,或者是否仅仅是TLB未命中,硬件页表行将找到映射(到零页面)。


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

只有8kiB的堆栈被引用,并由物理页面支持。 这是预料之中的,因为动态链接器不使用大量的堆栈。

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

触摸当前堆栈映射下方的内存,但在堆栈限制内, 会导致内核增长堆栈映射 (在页面错误处理程序中)。 (但是,只有当rsp被首先调整时, 红色区域只比rsp低128字节,所以ulimit -s unlimited不会在rsp下面触发内存1GB, 然后将堆栈扩展到那里, 但是如果你将rsp递减到那里,然后触摸内存 。)

这只适用于初始线程的堆栈。 pthreads只是使用mmap(MAP_ANONYMOUS|MAP_STACK)映射无法增长的mmap(MAP_ANONYMOUS|MAP_STACK)块。 ( MAP_STACK当前是不可操作的)。因此,线程堆栈在分配之后不能增长(除非手动使用MAP_FIXED如果下面有空间的话),并且不受ulimit -s unlimited影响。


这种魔法防止其他东西在堆栈增长区域中选择地址,对于mmap(MAP_GROWSDOWN)不存在,所以不要使用它来分配新的线程堆栈 。 (否则,最终可能会在新堆栈下面使用虚拟地址空间,导致无法增长)。 只分配完整的8MiB。 另请参见位于进程虚拟地址空间中的其他线程的堆栈在哪里? 。

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

触摸内存MAP_GROWSDOWN区域底部下的多个页面可能segfault(不同于Linux的主线程堆栈)。 针对Linux的编译器不会生成堆栈“探测器”,以确保在大分配(例如本地数组或分配)之后每个4k页面都被触摸,所以这是MAP_GROWSDOWN对堆栈不安全的另一个原因。

编译器在Windows上发出堆栈探测。

MAP_GROWSDOWN可能根本就不能工作,请参阅@ BeeOnRope的评论 ,因为堆栈冲突安全漏洞是可能的,如果映射增长接近其他事情,所以不要使用MAP_GROWSDOWN做任何事情我只是提到描述了Windows使用的guard-page机制,因为知道linux的主线程堆栈设计不是唯一可能的。

堆栈分配使用相同的虚拟内存机制来控制地址访问页面错误 。 即如果您当前的堆栈有7ffd41ad2000-7ffd41af3000作为界限:

 myaut@panther:~> grep stack /proc/self/maps 7ffd41ad2000-7ffd41af3000 rw-p 00000000 00:00 0 [stack] 

然后,如果CPU试图读取/写入地址为7ffd41ad1fff (堆栈顶部边界之前的1个字节)的数据,则会因为OS没有提供相应的分配内存块( 页面 )而产生页面错误 。 因此,使用%rsp作为地址的push或任何其他内存访问命令将触发pagefault

在页面错误处理程序中,内核将检查堆栈是否可以增长,如果超过,则会分配页面支持错误地址( 7ffd41ad1000-7ffd41ad2000 )或触发SIGSEGV。