使用“push”或“sub”x86 指令时如何分配堆栈内存?
- 2024-10-21 09:14:00
- admin 原创
- 95
问题描述:
我已经浏览了一段时间,并试图了解在执行以下示例时如何将内存分配给堆栈:
push rax
或者移动堆栈指针来为子程序的局部变量分配空间:
sub rsp, X ;Move stack pointer down by X bytes
我的理解是,堆栈段在虚拟内存空间中是匿名的,即不受文件支持。
我还了解到,内核不会真正将匿名虚拟内存段映射到物理内存,直到程序实际对该内存段执行某些操作(即写入数据)。因此,在写入之前尝试读取该段可能会导致错误。
在第一个例子中,如果需要,内核将在物理内存中分配一个框架页面。在第二个例子中,我假设内核不会为堆栈段分配任何物理内存,直到程序实际将数据写入堆栈段中的地址。
我在这条路上走对了吗?
解决方案 1:
是的,你基本上是在正确的轨道上。 sub rsp, X
有点像“惰性”分配:内核只在#PF
页面错误异常之后才执行任何操作,因为页面错误异常会触及新 RSP 上方的内存,而不仅仅是修改寄存器。但你仍然可以认为内存“已分配”,即可以安全使用。
因此,在写入之前尝试读取该段可能会导致错误。
不会,读取不会导致错误。从未写入的匿名页面会被写入时复制映射到物理零页,无论它们是在 BSS、堆栈还是 中mmap(MAP_ANONYMOUS)
。
有趣的事实:在微基准测试中,请确保为输入数组写入每一页内存,否则您实际上是在重复循环相同的物理 4k 或 2M 零页,并且会获得 L1D 缓存命中,即使您仍然会获得 TLB 未命中(和软页面错误)! 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
...
仅引用了 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 的主线程堆栈设计并不是唯一可能的设计。)
解决方案 2:
堆栈分配使用相同的虚拟内存机制来控制地址访问页面错误。例如,如果您当前的堆栈具有7ffd41ad2000-7ffd41af3000
以下边界:
myaut@panther:~> grep stack /proc/self/maps
7ffd41ad2000-7ffd41af3000 rw-p 00000000 00:00 0 [stack]
然后,如果 CPU 尝试在地址7ffd41ad1fff
(堆栈顶部边界前 1 个字节)处读取/写入数据,它将生成页面错误,因为操作系统没有提供相应的分配内存块(页面)。 因此push
或任何其他以地址为单位的内存访问命令%rsp
都将触发页面错误。
在页面错误处理程序中,内核将检查堆栈是否可以增长,如果可以,它将分配页面支持错误地址(7ffd41ad1000-7ffd41ad2000
)或触发 SIGSEGV(如果超出堆栈 ulimit)。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件