是的,你的想法基本上是正确的。
sub rsp, X
有点像“懒惰”分配:内核只有在触碰新的RSP上方的内存,并且出现
#PF
页故障异常后才会执行任何操作,而不仅仅是修改寄存器。但是你仍然可以认为内存已经“分配”,也就是可安全使用。
尝试在写入之前读取该段不会导致错误。
不,读取不会导致错误。从未被写入过的匿名页面会被写时复制映射到一个/多个物理零页面,无论它们是否在BSS、堆栈或
mmap(MAP_ANONYMOUS)
中。
趣闻:在微基准测试中,确保为输入数组的每个页面编写每个页面,否则您实际上正在重复循环相同的物理4k或2M页面的零,并且将获得L1D高速缓存命中,即使您仍然会遇到TLB未命中(和软页错误)! gcc将优化malloc + memset(0)为 calloc ,但 std :: vector 实际上将写入所有内存,无论您是否希望如此。 memset 在全局数组上不会被优化掉,因此可以使用它。(或非零初始化数组将在数据段中备份。)
请注意,我将忽略映射与有线之间的差异,即访问是否会触发软/次要页错误以更新页表,或者它只是TLB缺失,硬件页表遍历将找到映射(到零页面)。
但是,RSP下面的堆栈内存可能根本没有映射,因此在不移动RSP的情况下触摸它可能会导致无效的页面错误,而不是“次要”页面错误来解决写时复制。
栈内存有一个有趣的特点:栈大小限制大约为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
后; red-zone仅在rsp
下方128字节,因此ulimit -s unlimited
不会使触摸rsp
以下1GB的内存增长到那里,但如果您将rsp
减少到那里,然后触摸内存,则会这样做。)
这仅适用于初始/主线程的堆栈。
pthreads
只使用
mmap(MAP_ANONYMOUS|MAP_STACK)
映射一个 8MiB 的块,它无法增长。(
MAP_STACK
目前是无操作的。)因此,在分配后线程堆栈无法增长(除非在它们下面有空间手动使用
MAP_FIXED
),并且不受
ulimit -s unlimited
的影响。
这种防止其他内容在堆栈增长区选择地址的魔法并不存在于
mmap(MAP_GROWSDOWN)
,因此
不要使用它来分配新线程堆栈。(否则,你可能会得到某些使用了新堆栈下面的虚拟地址空间的东西,使其无法增长)。只需分配完整的8MiB。另请参见
进程虚拟地址空间中其他线程的堆栈位于何处?。
MAP_GROWSDOWN
确实有按需增长的功能,
在mmap(2)
手册页中有描述,但没有增长限制(除了接近现有映射),因此(根据手册页)它基于类似Windows使用的保护页面,而不是主线程的堆栈。
在Linux的主线程栈中,触及内存超出
MAP_GROWSDOWN
区域下面的多个页面可能会导致段错误,这与一些编译器不会生成堆栈“探测”有关。 堆栈“探测”是指在进行大型分配(例如局部数组或alloca)后,确保按顺序触摸每个4k页面的机制。因此,这也是
MAP_GROWSDOWN
不安全的另一个原因。
编译器对Windows发出的堆栈探测有所不同。
MAP_GROWSDOWN
可能根本无法工作,详见
@BeeOnRope's comment。由于映射靠近其他内容可能会导致堆栈冲突安全漏洞,因此从未很安全地用于任何内容。因此,请勿将
MAP_GROWSDOWN
用于任何内容。我保留提到Windows使用的守卫页机制的描述,因为了解Linux的主线程堆栈设计并不是唯一可能的方案是很有趣的。
MAP_GROWNDOWN
根本不起作用:当使用像这样的代码访问映射区域下方时,它总是出错。这似乎是一个新 bug。 - BeeOnRope__do_anonymous_page
,另一个是跳过守卫页的流程,最终在 x86 平台上进入[这里的 __do_page_fault](https://patchwork.kernel.org/patch/9796395/)。你可以看到代码通过检查
rsp处理了
MAP_GROWSDOWN的情况,因此你不能将其作为一般的“向下增长”区域,因为内核实际上正在检查
rsp` 是否“接近”该区域,否则就会出现错误。 - BeeOnRopersp
(当然编译器会这么做)。我曾经使用ulimit -s unlimited
可以将已分配的堆栈增加1GB,并且Linux很高兴地将堆栈增加到1GB。这只有在主进程堆栈位于VM空间顶部并且在碰到其他东西之前有大约10TB的空间时才有效:对于具有固定堆栈大小且不使用GROWDOWN
功能的pthreads
线程,此方法不起作用。 - BeeOnRope