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

时间:2017-10-17 12:54:29

标签: linux memory memory-management x86-64

我一直在浏览一段时间,我正在尝试了解内存如何分配到堆栈中,例如:

push rax

或移动堆栈指针为子例程的局部变量分配空间:

sub rsp, X    ;Move stack pointer down by X bytes 

据我所知,堆栈段在虚拟内存空间中是匿名的,即不支持文件。

我还理解的是,内核实际上不会将匿名虚拟内存段映射到物理内存,直到程序实际对该内存段执行某些操作,即写入数据。因此,在写入之前尝试读取该段可能会导致错误。

在第一个示例中,如果需要,内核将在物理内存中分配帧页面。 在第二个示例中,我假设内核不会将任何物理内存分配给堆栈段,直到程序实际将数据写入堆栈堆栈段中的地址。

我在这里走在正确的轨道上吗?

2 个答案:

答案 0 :(得分:2)

  

我在这里走在正确的轨道上吗?

是的,非常接近。

  

因此,在写入之前尝试读取该段可能会导致错误。

不,读取不会导致错误。从未写过的匿名页面是写入时复制映射到/物理零页面,无论它们是在BSS,堆栈还是mmap(MAP_ANONYMOUS)

有趣的事实:在微基准测试中,请确保触摸输入数组的每一页内存,否则您实际上会反复循环遍历同一物理4k或2M页面的零,即使您仍然仍然会获得L1D缓存命中得到TLB未命中(和软页错误)! gcc会将malloc + memset(0)优化为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时; 仅比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)。