有趣的异常

缘起

最近,在项目中遇到一个有趣的异常。在没附加调试器的情况下会直接崩溃。附加调试器后,会中断到调试器中,但是按 F5 继续运行后,程序还能继续执行。 interesting !你能猜出这是个什么异常吗?

初遇错误

在测试程序功能的时候,意外的崩溃了。以为是偶发事件,于是重新执行,依然会崩溃。一般遇到这种问题可以先考虑在调试器中运行,看看是否能重现。

上调试器

附加 vs 到目标进程,再次执行相应的功能,果然中断到了调试器中。简单查看调用栈及出错的代码,并没有显著异常(其实,有相当明显的提示,只是当时没有注意)。偶然点了 F5,没想到居然可以继续运行,有点意思。由于项目比较紧,就一直将就着挂着调试器运行测试代码。前些日子有点时间,再次看了下这个问题。

再次检查

再次在调试器中运行程序,异常果然再次发生了。这次心态比较平和,不急不躁,仔细一看异常提示信息,差点被自己气吐血。具体是什么异常呢?请看下图:

stack-overflow-exception

这不是栈溢出吗?这么明显的提示,当时怎么就给华丽丽的错过了呢?一定要仔细看提示啊!知道是栈溢出,后面的问题就不用查了。这里简单介绍一下 32 位程序的栈增长过程。

栈增长过程

默认情况下,每个线程有 1 MB 的栈空间,这 1 MB 的空间对应的虚拟内存(按页面组织的,页面大小一般是 4KB)并不是一开始就有对应的物理页面的,而是按需分配的。

开始时,最上方的两个虚拟页面(栈是向下增长的)有对应的物理页面,这两个页面有 PAGE_READWRITE 保护属性,其中第二个页面还额外包含 PAGE_GUARD 保护属性,是防护页面。当线程试图访问防护页面中的内存时,系统会得到通知 ,系统会去除第二个页面的 PAGE_GUARD 保护属性标志,为第三个页面调拨物理页面,然后为第三个页面设置 PAGE_GUARD 保护属性标志,以此类推。

1022 个页面是最后一个带 PAGE_GUARD 的页面,也就是最后一个保护页面。当线程访问第 1022 个页面的时候,系统会得到通知,系统会去除第 1022 个页面的 PAGE_GUARD 保护属性标志,为第 1023 个页面调拨物理页面,但是系统并不会为第 1023 个页面设置 PAGE_GUARD 保护属性标志,相反的,会抛出 EXCEPTION_STACK_OVERFLOW 异常,该异常对应的值是 0xC00000FD

系统永远不会为第 1024 个页面调拨物理页面。这样是为了保护进程的其它数据,使它们不会因为意外的内存写越界而遭到破坏。

现象解释

相信弄明白栈的增长过程,基本就明白了为什么在调试器中可以继续运行的原因了。当调试器收到栈溢出异常时,系统已经把保护页面的PAGE_GUARD 保护属性标志去除了,并且为下一个页面调拨了物理页面。在调试器中按 F5 继续运行,再次访问相同的地址已经不会产生异常。所以就可以继续运行了。但并不是所有情况下都可以这么幸运,当使用内存过多,访问到第 1024 个页面时,还是会报页面访问异常(对应的异常码是 0xC0000005)。

access-violation-exception-after-stack-overflow

总结

  • 一定要仔细看错误提示!

  • 遇到诡异的问题可以试试在调试器中运行程序。

参考资料

《Windows 核心编程(第 5 版)》

BianChengNan wechat
扫描左侧二维码关注公众号,扫描右侧二维码加我个人微信:)
0%