是的,您几乎走在正确的道路上。sub rsp, X
有点像“惰性”分配:内核仅在#PF
页面错误异常是由于触摸新 RSP 之上的内存而引起的,而不仅仅是修改寄存器。但您仍然可以考虑“已分配”内存,即可以安全使用。
因此,在写入该段之前尝试读取该段可能会导致错误。
不,读取不会导致错误。从未写入过的匿名页会被写入时复制映射到物理零页,无论它们是在 BSS、堆栈还是在mmap(MAP_ANONYMOUS)
.
有趣的事实:在微基准测试中,请确保为输入数组写入每一页内存,否则实际上会重复循环相同的物理 4k 或 2M 零页,并且会获得 L1D 缓存命中,即使您仍然会出现 TLB 未命中的情况(和软页面错误)! gcc 将优化 malloc+memset(0) 为calloc
, but std::vector
无论您是否愿意,实际上都会写入所有内存。memset
全局数组上的未优化,因此可以工作。 (或者非零初始化的数组将在数据段中进行文件支持。)
请注意,我忽略了映射与有线之间的区别。即访问是否会触发软/次要页错误来更新页表,或者是否只是 TLB 未命中并且硬件页表遍历将找到映射(到零页)。
但RSP以下的堆栈内存可能根本无法映射,因此在不先移动 RSP 的情况下触摸它可能是无效页面错误,而不是“次要”页面错误来解决写入时复制问题。
堆栈内存有一个有趣的变化:堆栈大小限制约为 8MB(ulimit -s
),但在 Linux 中,进程的第一个线程的初始堆栈是特殊的。例如我设置了一个断点_start
在 hello-world (动态链接)可执行文件中,并查看/proc/<PID>/smaps
for it:
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 虚拟地址空间内随机选择页面。
触摸当前堆栈映射下方但在堆栈限制内的内存 导致内核增加堆栈映射(在页面错误处理程序中)。
(But only if rsp首先被调整; the red-zone仅低于 128 字节rsp
, so ulimit -s unlimited
不使触摸内存低于1GBrsp
将堆栈增长到那里,但如果你减少它就会rsp到那里然后触摸内存.)
这仅适用于初始/主线程的堆栈. pthreads
只是使用mmap(MAP_ANONYMOUS|MAP_STACK)
映射无法增长的 8MiB 块。
(MAP_STACK
目前是无操作。)因此线程堆栈在分配后不能增长(除非手动使用MAP_FIXED
如果它们下面有空间),并且不受ulimit -s unlimited
.
这种阻止其他事物选择堆栈增长区域中的地址的魔法并不存在mmap(MAP_GROWSDOWN)
, so do not用它来分配新的线程堆栈。 (否则,最终可能会耗尽新堆栈下方的虚拟地址空间,使其无法增长)。只需分配完整的 8MiB。也可以看看进程虚拟地址空间中其他线程的堆栈位于哪里?.
MAP_GROWSDOWN
确实具有按需增长的功能,中描述的mmap(2)手册页,但没有增长限制(除了接近现有映射之外),因此(根据手册页)它基于 Windows 使用的保护页,而不是像主线程的堆栈。
触摸内存底部下方的多个页面MAP_GROWSDOWN
区域可能会出现段错误(与 Linux 的主线程堆栈不同)。针对 Linux 的编译器不会生成堆栈“探针”来确保在大分配(例如本地数组或分配)后按顺序触及每个 4k 页面,所以这是另一个原因MAP_GROWSDOWN
对于堆栈来说不安全。
编译器确实会在 Windows 上发出堆栈探测。
(MAP_GROWSDOWN
甚至可能根本不起作用,请参阅@BeeOnRope 的评论。它对于任何东西来说都不是很安全,因为如果映射变得接近其他东西,则可能存在堆栈冲突安全漏洞。所以只是不要使用MAP_GROWSDOWN
对于任何事情。我将在提及中描述 Windows 使用的保护页机制,因为有趣的是,Linux 的主线程堆栈设计并不是唯一可能的设计。)