据我了解,芯片上的 MMU 获取虚拟地址并将其转换为物理内存地址。 MMU 执行以下操作:
(1) 查阅进程特定的页表
(2) 如果虚拟地址对应的页在常驻集中,则将该地址转换为物理地址
(3) 如果虚拟地址对应的页面不在常驻集中,则生成页面错误,由内核处理
现在,我了解流程的几个部分的页面创建和删除需要系统调用,例如brk()
、sbrk()
、mmap()
和munmap()
。因此,每当进行这些系统调用时,内核总是有机会更新进程的页表。
然而,正在运行的进程可能会通过要求堆栈指针%rsp
减少 10,000 来增加堆栈区域,这可能需要分配几页来适应堆栈深度的增加。
如果我对 MMU 的理解是正确的,那么在%rsp
发生更改时,MMU 将不会生成页面错误(因为该地址一开始就不在进程表中)。在这种情况下,MMU 会做什么来通知内核?
答案1
正在运行的进程可能会通过要求堆栈指针 %rsp 减少 10,000 来增加堆栈区域,这可能需要分配几页来适应堆栈深度的增加。
如果我对 MMU 的理解是正确的,那么在 %rsp 更改的情况下,MMU 将不会生成页面错误(因为该地址一开始就不在进程表中)。在这种情况下,MMU 会做什么来通知内核?
更改 %rsp 绝不会导致页面错误。页面错误仅在读取或写入内存时发生。
触摸未映射的页面将总是触发页面错误,但内核的页面错误处理程序可以确定这是一个“有效”页面错误,并增加逻辑映射+将页面连接到硬件页表中。或者判定它“无效”并发送 SIGSEGV。
堆栈增长是一个特例。通常,页面错误只有在现有映射内部时才有效(例如,延迟分配、写入时复制,或者页面被调出到交换空间或支持它的任何文件)。由于映射的逻辑状态不必与硬件页表匹配,因此会出现“软”或“硬”页错误。看使用“push”或“sub”x86 指令时,堆栈内存是如何分配的?更多细节。
某些实现有特殊情况来扩展堆栈映射。其他人则不这样做,并提前映射整个堆栈区域。然后页面错误将以与 相同的方式工作mmap()
。打电话的mmap()
具体方法是线程堆栈已分配。至少,当您使用以下命令创建 pthread 时,这是默认设置glibc。相比之下,初始线程的堆栈由内核创建,实现允许其增长的特殊情况。有关 Linux 如何增长堆栈的进一步讨论,请参阅下文。
如果您跳过堆栈末尾,则完全有可能导致对不同映射的杂散访问。即,您可以在不触发任何页面错误的情况下损坏该内存。这是一个“堆栈冲突”安全漏洞。恶意输入可能会导致这种情况发生,例如导致alloca
分配非常大的数组或 C99 可变长度数组,从而使堆栈指针跳过内容。
保证检测到所有此类堆栈溢出的方法是 1) 在堆栈末尾映射一个“保护页” 2) 在分配堆栈内存时一次探测一个页面。在撰写本文时,GCC 不完全支持生成堆栈探针。
x86-64 System V ABI 不需要堆栈探针来确保堆栈每次增长超过 1 页的正确性。因此,如果您明确告知 gcc,它只会发出堆栈探测。 (我认为 Linux 在大多数非 x86 架构上使用的 ABI 是相同的。)在 Linux 上,仅需要堆栈探测来确保在尝试将堆栈增长到超出堆栈大小限制末尾时程序会出错。
(有趣的事实:Windows做在堆栈上分配大数组时需要按顺序触摸每个页面。当将堆栈指针移动超过一页或移动可变量时,针对 Windows 的编译器确实需要始终发出堆栈探针以确保正确性。可以大于一页。)
为什么要费心去增加堆栈呢?
使用固定大小的堆栈映射似乎至少有一个缺点。当您使用 mmap() 创建这样的堆栈时,尽管它不分配物理内存,但 Linux 会将其视为“已提交”内存。
默认情况下,Linux 允许 RAM+swap 过度使用,但使用启发式方法拒绝“明显的地址空间过度使用”。当你真正尝试时使用当内存超过 RAM+swap 时,“内存不足杀手”(OOM) 将开始选择正在运行的程序进行屠杀,直到您有足够的内存。可以配置不同的策略,例如拒绝分配,这将导致提交超过 RAM/2 + 交换。
看vm.overcommit_memory 和 vm.overcommit_ratio。
下面的 Windows 博客文章中也提到了这一注意事项。也许实现上的差异是人们对 Linux 过度使用和 OOM 杀手的抱怨的影响因素之一:-)。
[C 运行时,例如 glibc]可以使其大部分最初不可写/不可读取,并在出现故障时进行更改,但随后您需要信号处理程序,并且此解决方案在 POSIX 线程实现中是不可接受的,因为它会干扰应用程序的信号处理程序。 --StackOverflow 上的用户“R..”
Linux 提供了一种替代机制,MAP_GROWSDOWN
.它基本上与上面的引用相同,但在内核中实现。这是内核在为进程创建初始堆栈时使用的内容。然而,这才真正有意义,因为 Linux 还为主堆栈保留虚拟内存以增长,最多可达 的值ulimit -s
。一些使其安全+正确工作的“魔法”无法通过 获得mmap(MAP_GROWSDOWN)
,因此它不适用于线程堆栈。否则这将是一个有效的选择。
“R..”继续建议改变内核以支持线程堆栈的按需提交。
各类参考资料:
- Linux 上的 LLVM 没有实现堆栈探测。这成为了一个突出问题对于内存安全的 Rust 语言,现已解决(“适用于所有 1 级平台”)。
- 一些消息来源提到 GCC 实现了堆栈探测,但前提是您通过了
-fstack-check
. “不幸的是,-fstack-check
实际上不太适合我们的目的”几个原因。 - 最近:“大多数目标不完全支持堆栈冲突保护。但是,在这些目标上将
-fstack-clash-protection
保护动态堆栈分配。-fstack-clash-protection
如果目标支持,还可以为静态堆栈分配提供有限的保护-fstack-check=specific
”。 - CVE-2010-2240- Linux 终于意识到需要在
MAP_GROWSDOWN
. - 堆栈冲突2017 年 - Linux 终于意识到,如果没有堆栈探测器,一页是不够的。
- 强制性的LWN.net 关于 StackClash 的文章。
- Raymond Chen 的博客文章的一部分解释了 Windows 的实现:https://devblogs.microsoft.com/oldnewthing/?p=29563