我已经浏览了一段时间,我试图了解在执行以下操作时如何将内存分配给堆栈:
push rax
或者移动堆栈指针为子程序的局部变量分配空间:
sub rsp, X ;Move stack pointer down by X bytes
我的理解是堆栈段在虚拟内存空间中是匿名的,即不是文件支持的。
我还理解的是,内核实际上不会将匿名虚拟内存段映射到物理内存,直到程序实际对该内存段执行某些操作,即写入数据。因此,在写入之前尝试读取该段可能会导致错误。
在第一个示例中,如果需要,内核将在物理内存中分配一个帧页面。
在第二个示例中,我假设内核不会将任何物理内存分配给堆栈段,直到程序实际将数据写入堆栈段中的地址。
我在正确的轨道上吗?
最佳答案
是的,你在这里走在正确的轨道上,几乎。 sub rsp, X
有点像“懒惰”分配:内核仅在 #PF
页面错误异常发生后才执行任何操作,因为它会接触新 RSP 上方的内存,而不仅仅是修改寄存器。但是您仍然可以考虑“分配”的内存,即可以安全使用。
So, trying to read that segment before writing to it may cause an error.
不,读取不会导致错误。从未被写入的匿名页面被映射到一个/物理零页面的写时复制,无论它们是在 BSS、堆栈还是
mmap(MAP_ANONYMOUS)
中。有趣的事实:在微基准测试中,确保为输入数组写入每一页内存,否则您实际上是在重复循环相同的物理 4k 或 2M 页的零,并且即使您仍然会遇到 TLB 未命中,也会获得 L1D 缓存命中(和软页面错误)! 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 虚拟地址空间内随机选择页面。接触低于当前堆栈映射但在堆栈限制内的内存 causes the kernel to grow the stack mapping(在页面错误处理程序中)。
(但是 only if
rsp
is adjusted first ; red-zone 仅比 rsp
低 128 个字节,因此 ulimit -s unlimited
不会使接触内存低于 rsp
1GB 到那里,67914)这仅适用于初始/主线程的堆栈 。
rsp
只是使用 pthreads
来映射一个无法增长的 8MiB 块。(
mmap(MAP_ANONYMOUS|MAP_STACK)
目前是一个空操作。)所以线程堆栈在分配后不能增长(除非手动使用 MAP_STACK
如果它们下面有空间),并且不受 MAP_FIXED
的影响。ulimit -s unlimited
不存在这种阻止其他事物选择堆栈增长区域中地址的魔法,因此 but it will if you decrement mmap(MAP_GROWSDOWN)
to there and then touch memory 不存在。 (否则,您最终可能会占用新堆栈下方的虚拟地址空间,使其无法增长)。只需分配完整的 8MiB。另见 do not use it to allocate new thread stacks 。MAP_GROWSDOWN
确实具有按需增长功能 Where are the stacks for the other threads located in a process virtual address space? ,但没有增长限制(除了接近现有映射),因此(根据手册页)它基于像 Windows 使用的保护页面,而不是像主线程的堆栈。触摸
mmap(2)
区域底部下方的多个页面可能会出现段错误(与 Linux 的主线程堆栈不同)。针对 Linux 的编译器不会生成堆栈“探测”以确保在大分配(例如本地数组或 alloca)之后按顺序访问每个 4k 页,因此这是 MAP_GROWSDOWN
对堆栈不安全的另一个原因。编译器确实会在 Windows 上发出堆栈探测。
(
MAP_GROWSDOWN
甚至可能根本不起作用,请参阅 described in the MAP_GROWSDOWN
man page 。用于任何事情从来都不是很安全,因为如果映射变得接近于其他东西,堆栈冲突安全漏洞是可能的。所以永远不要将 MAP_GROWSDOWN
用于任何事情。我留在这里是为了描述 Windows 使用的保护页机制,因为有趣的是,Linux 的主线程堆栈设计并不是唯一可能的。)
关于linux - 使用 'push' 或 'sub' x86 指令时如何分配堆栈内存?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46790666/