当本地变量未初始化时,Windows线程堆栈保护页面机制是如何工作的?

4
在Windows x86-32/x86-64架构的操作系统中,线程堆栈虚拟内存由“保留部分”、“提交部分”、“守卫页”和“保留页”组成。
问题:
假设我有1页提交内存和1MB的线程堆栈保留内存。我在堆栈上分配了一些内存,等于K页而没有初始化。例如,K等于10。看起来,在堆栈帧的开始,用户空间代码将分配堆栈上的内存:
sub esp, K*4096

当存在对保护页的读写请求时,Guard Page机制会起作用。

但是,如果我要读写超出此保护页的某些内存,会发生什么?


一个页面错误中断,我猜.. - Martin James
2
好问题。我不确定最近的操作系统版本如何工作,但我确实知道几年前我使用的编译器必须进行修改,以便在分配大量本地变量时发出代码,实际上“触摸”该内存的每个页面,以精确地强制发生保护页故障并分配堆栈内存。如果没有这种更改,则触及保护页之外的内存将导致应用程序崩溃(无痕迹消失)。 - 500 - Internal Server Error
2Martin:在这种情况下,您应该确定来自“需要更多提交”的内存的非法访问。但是无论如何,感谢您的建议。 - Konstantin Burlachenko
顺便提一下,在Linux上,您不需要触及中间页面。如果您触及任何保留用作堆栈的页面,即使尚未映射,页面错误处理程序也会增加堆栈映射。 (这仅适用于Linux上的初始进程堆栈。Linux上的线程堆栈需要完全分配其空间,以防止其他分配占用它们将来需要的空间。但是,它们仍然可以懒惰地连接到实际的页表中(页面错误而不仅仅是TLB缺失)。) - Peter Cordes
2个回答

4
您通常会开始测试启用了运行时检查的代码。在Debug配置中默认启用的MSVC++上的/RTC,它会在函数序言中注入对_chkstk()的调用。GCC/g++有非常相似的功能
这会在函数序言中探测分配页面,每隔4096个字节读取一次。这可以确保当您出错时总是会触发保护页,从而触发此站点名称并帮助您修复错误。
如果没有进行这种检查,您可能会在根本不属于堆栈的页面上寻址。虽然很可能会触发处理器的#GP陷阱,但这并不是保证的,因为该页面可能已被另一个无关的分配映射。您必须很不幸才能遇到这种情况,这是一种根本性的UB,非常难以诊断,因为您从未怀疑过堆栈,/RTC非常有价值。

4
这个答案似乎错误地表明,打开栈警卫页是一个终端失败(“...修复错误”),而且当启用运行时检查时才会发出 _chkstk()。只要栈可以扩展,打开栈警卫页就会默默地增加堆栈大小(如果有足够的空间),并为现在更大的堆栈创建一个新的警卫页。只要栈可以扩展,用户进程中不会生成异常。因此,MSVC++编译器总是在存在多于1页本地变量以触发这种扩展机制并确保栈足够大时发出 _chkstk() - Iridium
这不正确,触发守卫页总是会引发堆栈溢出异常(异常代码0xc00000fd)。代码可以捕获该SEH异常并处理不良事件,通常通过重新映射守卫页提供额外的~7KB空间。让程序继续而不是用诊断终止它是不明智的,但在技术上是可能的,必须使用_resetstkoflw()恢复守卫。 - Hans Passant

1
当访问超出保护页的地址时,您的程序将崩溃,但默认情况下编译器每次本地分配超过4K时都会调用__chkstk()函数。
这是一篇介绍Windows系统中堆栈保护页如何工作的文章: kb100775

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接