linux - 使用 'push' 或 'sub' x86 指令时如何分配堆栈内存?

标签 linux memory memory-management x86-64

我已经浏览了一段时间,我试图了解在执行以下操作时如何将内存分配给堆栈:

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 仅比 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 stacksMAP_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/

相关文章:

linux - 红帽许可证

linux - 仅删除包含卡详细信息的行

c - 函数中的 Malloc 和 free(优化你的内存)

iOS : ARC, 未释放内存

java - OutOfMemoryError - 我可以将数据转储到文件而不是内存中吗?

linux - 在 Perl 中写入串口

windows - 适用于 Windows、Linux、MacOS X 的跨平台脚​​本

ios - 由于内部保留周期,ViewController 没有被释放

c - 如何使用指向切片字符串的指针的指针

c++ - 有没有新的 _malloca 等价物