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

Posted

技术标签:

【中文标题】使用 \'push\' 或 \'sub\' x86 指令时如何分配堆栈内存?【英文标题】:How is Stack memory allocated when using 'push' or 'sub' x86 instructions?使用 'push' 或 'sub' x86 指令时如何分配堆栈内存? 【发布时间】:2018-03-29 04:13:24 【问题描述】:

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

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 的情况下触摸它可能是一个无效页面错误,而不是一个“次要”页面错误来解决 copy-on-写。


堆栈内存有一个有趣的变化:堆栈大小限制大约为 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 的堆栈增长到那里,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 对堆栈不安全的另一个原因。

编译器确实会在 Windows 上发出堆栈探测。

(MAP_GROWSDOWN 甚至可能根本不起作用,请参阅@BeeOnRope's comment。用于任何事情从来都不是很安全,因为如果映射变得接近其他东西,堆栈冲突安全漏洞是可能的。所以不要任何事情都可以使用MAP_GROWSDOWN。我将不再提及以描述 Windows 使用的保护页面机制,因为有趣的是,知道 Linux 的主线程堆栈设计并不是唯一可能的。)

【讨论】:

Linux 不使用保护页面来增加堆栈(实际上直到最近才出现与堆栈相关的任何称为“保护页面”的东西)。编译器不需要“探测”堆栈,因此您可以跳过映射页面并毫无问题地触摸堆栈“末端”附近的页面(所有中间页面也被映射)。有趣的是,Windows确实按照您的描述工作:它有一个 [0] 保护页面,并且触摸该页面,将触发堆栈的扩展,并设置一个新的保护页面。 彼得·科德斯。我已经对其进行了更多研究,答案似乎是“它很复杂,但文档可能是错误的”。在我的盒子上,在堆栈上分配大量数据并深入其中(即,一个低得多的地址)跳过许多页面可以正常工作。这与我在内核源代码中的检查一致。在我的盒子上MAP_GROWNDOWN 根本不起作用:在使用code like this 访问映射区域下方时它总是出错。这似乎是一个new bug。 据我所知,基本上有两个流通过内核:一个到达保护页面,最终到达__do_anonymous_page,另一个是跳过保护页面时的流,最后是here in __do_page_fault for x86。在那里你可以看到代码处理MAP_GROWSDOWN 的情况并检查rsp: 所以你根本不能把它用作一般的“成长”区域,因为内核实际上正在检查rsp 是“靠近”这个区域,否则会出错。 最后,这也回答了你上面的一个问题:被认为是“堆栈增长区域”的区域似乎是任意大的,只要先调整rsp(编译器会这样做,当然)。我能够在当前分配的堆栈之外写入 1 GB(使用ulimit -s unlimited),Linux 很高兴将堆栈增加到 1 GB。这只有效,因为主进程堆栈位于 VM 空间的顶部,大约有 10 TB,然后才会碰到其他任何东西:这不适用于具有固定堆栈大小且不使用 @987654369 的 pthreads 线程@ 东西。 @BeeOnRope:感谢您的所有研究,我的回答与其中几个 cmets 相关。【参考方案2】:

堆栈分配使用相同的虚拟内存机制来控制地址访问pagefault。 IE。如果您当前的堆栈以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) 或在超出堆栈 ulimit 时触发 SIGSEGV。

【讨论】:

以上是关于使用 'push' 或 'sub' x86 指令时如何分配堆栈内存?的主要内容,如果未能解决你的问题,请参考以下文章

Android 逆向x86 汇编 ( add / sub / mul / div 数值运算指令 | xor / not / sal / sar / shl / shr 位运算指令 )

计算机组成的一些总结X86指令简介

计算机组成的一些总结X86指令简介

Android 逆向x86 汇编 ( push / pop 入栈 / 出栈 指令 | ret / retn 函数调用返回指令 | set 设置目标值指令 )

Android 逆向x86 汇编 ( push / pop 入栈 / 出栈 指令 | ret / retn 函数调用返回指令 | set 设置目标值指令 )

x86汇编指令集大全