使用 '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 位运算指令 )
Android 逆向x86 汇编 ( push / pop 入栈 / 出栈 指令 | ret / retn 函数调用返回指令 | set 设置目标值指令 )
Android 逆向x86 汇编 ( push / pop 入栈 / 出栈 指令 | ret / retn 函数调用返回指令 | set 设置目标值指令 )