有趣的異常
緣起
最近,在項目中遇到一個有趣的異常。在沒附加調(diào)試器的情況下會直接崩潰。附加調(diào)試器后,會中斷到調(diào)試器中,但是按 F5 繼續(xù)運行后,程序還能繼續(xù)執(zhí)行。interesting !你能猜出這是個什么異常嗎?
初遇錯誤
在測試程序功能的時候,意外的崩潰了。以為是偶發(fā)事件,于是重新執(zhí)行,依然會崩潰。一般遇到這種問題可以先考慮在調(diào)試器中運行,看看是否能重現(xiàn)。
上調(diào)試器
附加 vs 到目標進程,再次執(zhí)行相應(yīng)的功能,果然中斷到了調(diào)試器中。簡單查看調(diào)用棧及出錯的代碼,并沒有顯著異常(其實,有相當明顯的提示,只是當時沒有注意)。偶然點了 F5,沒想到居然可以繼續(xù)運行,有點意思。由于項目比較緊,就一直將就著掛著調(diào)試器運行測試代碼。前些日子有點時間,再次看了下這個問題。
再次檢查
再次在調(diào)試器中運行程序,異常果然再次發(fā)生了。這次心態(tài)比較平和,不急不躁,仔細一看異常提示信息,差點被自己氣吐血。具體是什么異常呢?請看下圖:

這不是棧溢出嗎?這么明顯的提示,當時怎么就給華麗麗的錯過了呢?一定要仔細看提示??!知道是棧溢出,后面的問題就不用查了。這里簡單介紹一下 32 位程序的棧增長過程。
棧增長過程
默認情況下,每個線程有 1 MB 的??臻g,這 1 MB 的空間對應(yīng)的虛擬內(nèi)存(按頁面組織的,頁面大小一般是 4KB)并不是一開始就有對應(yīng)的物理頁面的,而是按需分配的。
開始時,最上方的兩個虛擬頁面(棧是向下增長的)有對應(yīng)的物理頁面,這兩個頁面有 PAGE_READWRITE 保護屬性,其中第二個頁面還額外包含 PAGE_GUARD 保護屬性,是防護頁面。當線程試圖訪問防護頁面中的內(nèi)存時,系統(tǒng)會得到通知 ,系統(tǒng)會去除第二個頁面的 PAGE_GUARD 保護屬性標志,為第三個頁面調(diào)撥物理頁面,然后為第三個頁面設(shè)置 PAGE_GUARD 保護屬性標志,以此類推。
第 1022 個頁面是最后一個帶 PAGE_GUARD 的頁面,也就是最后一個保護頁面。當線程訪問第 1022 個頁面的時候,系統(tǒng)會得到通知,系統(tǒng)會去除第 1022 個頁面的 PAGE_GUARD 保護屬性標志,為第 1023 個頁面調(diào)撥物理頁面,但是系統(tǒng)并不會為第 1023 個頁面設(shè)置 PAGE_GUARD 保護屬性標志,相反的,會拋出 EXCEPTION_STACK_OVERFLOW 異常,該異常對應(yīng)的值是 0xC00000FD。
系統(tǒng)永遠不會為第 1024 個頁面調(diào)撥物理頁面。這樣是為了保護進程的其它數(shù)據(jù),使它們不會因為意外的內(nèi)存寫越界而遭到破壞。
現(xiàn)象解釋
相信弄明白棧的增長過程,基本就明白了為什么在調(diào)試器中可以繼續(xù)運行的原因了。當調(diào)試器收到棧溢出異常時,系統(tǒng)已經(jīng)把保護頁面的PAGE_GUARD 保護屬性標志去除了,并且為下一個頁面調(diào)撥了物理頁面。在調(diào)試器中按 F5 繼續(xù)運行,再次訪問相同的地址已經(jīng)不會產(chǎn)生異常。所以就可以繼續(xù)運行了。但并不是所有情況下都可以這么幸運,當使用內(nèi)存過多,訪問到第 1024 個頁面時,還是會報頁面訪問異常(對應(yīng)的異常碼是 0xC0000005)。

總結(jié)
一定要仔細看錯誤提示!
遇到詭異的問題可以試試在調(diào)試器中運行程序。
參考資料
《Windows 核心編程(第 5 版)》
