我一直在浏览一段时间,我正在尝试了解内存如何分配到堆栈中,例如:
push rax
或移动堆栈指针为子例程的局部变量分配空间:
sub rsp, X ;Move stack pointer down by X bytes
据我所知,堆栈段在虚拟内存空间中是匿名的,即不支持文件。
我还理解的是,内核实际上不会将匿名虚拟内存段映射到物理内存,直到程序实际对该内存段执行某些操作,即写入数据。因此,在写入之前尝试读取该段可能会导致错误。
在第一个示例中,如果需要,内核将在物理内存中分配帧页面。 在第二个示例中,我假设内核不会将任何物理内存分配给堆栈段,直到程序实际将数据写入堆栈堆栈段中的地址。
我在这里走在正确的轨道上吗?
答案 0 :(得分:2)
我在这里走在正确的轨道上吗?
是的,非常接近。
因此,在写入之前尝试读取该段可能会导致错误。
不,读取不会导致错误。从未写过的匿名页面是写入时复制映射到/物理零页面,无论它们是在BSS,堆栈还是mmap(MAP_ANONYMOUS)
。
calloc
,但std::vector
实际上会写入所有内存,无论你是否想要它。全局数组上的memset
未进行优化,因此可行。 (或者非零初始化数组将在数据段中进行文件支持。)
注意,我忽略了映射与有线之间的区别。即,访问是否会触发软页面错误以更新页面表,或者它是否仅仅是TLB未命中,并且硬件页表行走将找到映射(到零页面)。
堆栈内存有一个有趣的转折:堆栈大小限制类似于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虚拟地址空间内的页面,堆栈可能会增长到
触摸当前堆栈映射下方但在堆栈限制内的内存 causes the kernel to grow the stack mapping(在页面错误处理程序中)。 (但仅在首先调整rsp
时; red-zone仅比rsp
低128个字节,因此ulimit -s unlimited
不会使触摸内存低于rsp
增长1GB堆栈到那里,but it will if you decrement rsp
to there and then touch memory。)
这仅适用于初始线程的堆栈。 pthreads
只使用mmap(MAP_ANONYMOUS|MAP_STACK)
来映射无法增长的8MiB块。
(MAP_STACK
目前是无操作。)所以线程堆栈在分配后不能增长(除非MAP_FIXED
手动,如果它们下面有空格),并且不受ulimit -s unlimited
的影响
mmap(MAP_GROWSDOWN)
不存在阻止其他事物在堆栈增长区域中选择地址的魔法,因此do not use it to allocate new thread stacks。 (否则你最终可能会因使用新堆栈下方的虚拟地址空间而无法增长)。只需分配完整的8MiB即可。另请参阅Where are the stacks for the other threads located in a process virtual address space?。
MAP_GROWSDOWN
确实有一个按需增长功能described in the mmap(2)
man page,但没有增长限制(除了接近现有的映射),因此(根据手册页)它是基于在Windows使用的防护页面上,而不是主线程的堆栈。
触摸MAP_GROWSDOWN
区域底部下方的多个页面可能会出现段错误(与Linux的主要线程堆栈不同)。针对Linux的编译器不会生成堆栈“探测器”以确保在大量分配(例如本地阵列或alloca)之后按顺序触摸每个4k页面,因此这是MAP_GROWSDOWN
对堆栈不安全的另一个原因。 / p>
编译器会在Windows上发出堆栈探测。
(MAP_GROWSDOWN
可能根本不起作用,请参阅@BeeOnRope's comment。用于任何事情从来都不是非常安全,因为如果映射增长接近其他东西,则堆栈冲突安全漏洞是可能的。所以只是不要使用MAP_GROWSDOWN
进行任何操作。我将在提及中描述Windows使用的防护页面机制,因为知道Linux的主线程堆栈设计不是唯一的可能是有趣的。)
答案 1 :(得分:1)
堆栈分配使用相同的虚拟内存机制来控制地址访问 pagefault 。即如果当前堆栈的边界为7ffd41ad2000-7ffd41af3000
:
myaut@panther:~> grep stack /proc/self/maps
7ffd41ad2000-7ffd41af3000 rw-p 00000000 00:00 0 [stack]
然后,如果CPU尝试读取/写入地址7ffd41ad1fff
(堆栈顶部边界前1个字节)的数据,它将生成 pagefault ,因为OS没有提供相应的已分配内存块(页面)。因此,push
或任何其他以%rsp
作为地址的内存访问命令将触发 pagefault 。
在pagefault处理程序中,内核将检查堆栈是否可以增长,如果是,它将分配页面支持错误地址(7ffd41ad1000-7ffd41ad2000
)或触发SIGSEGV(如果超出堆栈ulimit)。