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)
据我了解,堆栈段在虚拟内存空间中是匿名的,即不是文件支持的。
我还了解的是,内核不会真正将匿名虚拟内存段映射到物理内存,直到程序对该内存段进行实际操作(即写入数据)为止。因此,尝试在写入该段之前先读取该段可能会导致错误。
在第一个示例中,如果需要,内核将在物理内存中分配一个帧页。在第二个示例中,我假设内核不会在程序实际将数据写入堆栈堆栈段中的地址之前将任何物理内存分配给堆栈段。
我在正确的轨道上吗?
是的,您在这里的方向正确。 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的主线程堆栈设计并不是唯一的可能。)