有趣的异常
共 1450字,需浏览 3分钟
·
2020-10-21 18:22
缘起
最近,在项目中遇到一个有趣的异常。在没附加调试器的情况下会直接崩溃。附加调试器后,会中断到调试器中,但是按 F5
继续运行后,程序还能继续执行。interesting
!你能猜出这是个什么异常吗?
初遇错误
在测试程序功能的时候,意外的崩溃了。以为是偶发事件,于是重新执行,依然会崩溃。一般遇到这种问题可以先考虑在调试器中运行,看看是否能重现。
上调试器
附加 vs
到目标进程,再次执行相应的功能,果然中断到了调试器中。简单查看调用栈及出错的代码,并没有显著异常(其实,有相当明显的提示,只是当时没有注意)。偶然点了 F5
,没想到居然可以继续运行,有点意思。由于项目比较紧,就一直将就着挂着调试器运行测试代码。前些日子有点时间,再次看了下这个问题。
再次检查
再次在调试器中运行程序,异常果然再次发生了。这次心态比较平和,不急不躁,仔细一看异常提示信息,差点被自己气吐血。具体是什么异常呢?请看下图:
这不是栈溢出吗?这么明显的提示,当时怎么就给华丽丽的错过了呢?一定要仔细看提示啊!知道是栈溢出,后面的问题就不用查了。这里简单介绍一下 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
)。
总结
一定要仔细看错误提示!
遇到诡异的问题可以试试在调试器中运行程序。
参考资料
《Windows 核心编程(第 5 版)》