——————————————————————————————————————————————————————————————————————————
前一篇指出 tail_recursivef_factorial() 會遞歸調用自身來計算某個正整數的階乘。當要計算的目標數值過大,經歷多次調用后,
就會耗盡可用的內核棧,引發一次頁錯誤異常,而轉移控制到錯誤處理程序前再次向無效的內存地址壓入“陷阱幀”則會讓原本可
以處理的異常升級為“double fault”,致使系統崩潰。本篇通過試圖計算 685! 來觸發“double fault”並進行分析。
將編譯好的驅動拷貝到被調試機器上,利用 sc.exe 把它加載至內核空間,源碼中(參見上一篇)設置的初始斷點被激活從而斷入
調試機上的 WinDbg.exe,觀察驅動入口點“DriverEntry()”內的局部變量,其中“Number”的值 0x2ad 正是要計算階乘的數
685:
按下“g”鍵恢復執行,沒多久就讓系統崩潰了,這在我們的意料之中,如果沒有連接宿主機上的調試器,目標系統就會直接
藍屏,並且顯示“bug check”代碼——0000007F:
在 MSDN 網站上搜索該錯誤碼,它對應於“UNEXPECTED_KERNEL_MODE_TRAP”,官方給出的解釋如下:
The UNEXPECTED_KERNEL_MODE_TRAP bug check has a value of 0x0000007F.
This bug check indicates that the Intel CPU generated a trap and the kernel failed to catch this trap.This trap could be a bound trap (a trap the kernel is not permitted to catch) or a double fault
(a fault that occurred while processing an earlier fault, which always results in a system failure).這種錯誤是由於 Intel CPU 生成了一個陷阱(trap),而內核未能捕獲這個陷阱。
此陷阱可能是一個受困陷阱(內核不允許捕獲的陷阱),或一個“double fault”(當處理一個早先的錯誤時又出現一個錯誤,
這樣就總是會導致系統故障)。
原文描述中的后一種情況(處理錯誤時又發生另一個錯誤)就是我們此刻的處境。
UNEXPECTED_KERNEL_MODE_TRAP 有四個參數,你可以從上一張圖看到,首個參數值為“0x00000008(陷阱編號)”,
官方對該值的解釋為:
0x00000008, or Double Fault, indicates that an exception occurs during a call to the handler for a prior exception.
Typically, the two exceptions are handled serially.
However, there are several exceptions that cannot be handled serially,
and in this situation the processor signals a double fault. There are two common causes of a double fault:A kernel stack overflow. This overflow occurs when a guard page is hit, and the kernel tries to push a trap frame.
Because there is no stack left, a stack overflow results, causing the double fault.
If you think this overview has occurred, use !thread to determine the stack limits, and then use kb
(Display Stack Backtrace) with a large parameter (for example, kb 100) to display the full stack.A hardware problem.
“Double Fault”,指明在調用前一個異常處理程序期間,又出現了一個異常。一般而言,兩個異常是順序處理的。
然而,有一些異常無法順序處理,在這種情況下處理器就會發出一個“double fault”信號。有兩種常見情況會導致
“double fault”:1。一次內核棧溢出。當接觸到一個保護頁時就會發生此類溢出,然后內核試圖向其中壓入一個陷阱幀。
因為已經沒有剩余棧可用了,導致又一次棧溢出,造成“double fault”。如果你認為發生了這種溢出,利用“!thread”調試器
命令確定棧界限,然后使用“kb”(顯示棧回溯)命令,並帶着較大的參數(比如 kb 100)來顯示完整的棧。2。硬件問題
遵循原文的指示,我們先檢查當前線程的棧界限,然后執行棧回溯看是否真的越界了,如下圖所示,內核棧邊界在 8bf47000 處,
而發生異常前的最后一次遞歸調用的幀指針(ChildEBP)為 8bf47008 ,已經快要出界了:
“nt!KiTrap08”是實際的陷阱處理程序,有趣的是前面的陷阱編號(0x00000008)就在這個例程的名字中,這絕不是巧合,
實際上“nt!KiTrap08”就是“double fault”專用的異常處理程序!
傳遞給它的第三個參數“801d8940”同時也就是 UNEXPECTED_KERNEL_MODE_TRAP 的第二個參數,它是一
個“nt!_NT_TIB”結構的“SubSystemTib”字段值:
其實這個字段中包含的信息對於我們此刻的故障排查而言並不那么重要,只是怕有人好奇它的來龍去脈,才略作說明罷了。
上圖中的 nt!KiTrap08 棧幀名稱后面給出了一個 TSS(任務狀態段)的段選擇符為 28。這才是關鍵的信息,通過它可會回到
事故現場,分析異常發生時的上下文。這個段選擇符存儲在“nt!_KTSS”結構的首個字段(Backlink)內:
看到這里應該能夠稍稍體會出內核中相關數據結構設計的多么用心良苦!
放下我們的多愁善感,利用調試器的“.tss”命令,后接段選擇符,即可回到事故現場,如下所示,異常發生時,EIP 指向即將執行
的指令地址為 977bd06f,換言之是該地址處的“前一條”指令(push ecx)導致的異常,為啥這條壓棧指令會導致異常呢?
你看“那時”的 esp 已經指向了內核棧的邊界點(8bf47000),而壓棧指令需要先把 esp 值減去 4 字節,然后再把 ecx 的內容
寫入 8BF46FFC 地址處,該地址已經位於邊界之外。
還記得前一篇我們計算出每次遞歸調用都會消耗掉 16 字節的內核棧空間嗎?這出錯前的最后一次調用中,試圖消耗的最后 4 字節
就在邊界之外!
低於 8bf47000 的虛擬內存沒有分配實際的物理頁,而且我們模擬對 8BF46FFC 執行物理地址轉譯也失敗,證實是由於訪問到
無效地址引發異常的(CR2 寄存器存儲導致頁錯誤的訪問地址):
如前所述,頁錯誤發生后,在控制權轉移到 nt!KiTrap08 之前,再次向這個無效的地址壓入一張“陷阱幀”,導致再度出現錯誤,
而 nt!KiTrap08 通過傳遞給它的首個參數(0x0000007f)明白了這是一個“double fault”,所以調用 nt!KiBugCheck,后者
探測調試器是否存在,決定是要繪制藍屏還是斷入調試器。這就是前面那張棧回溯輸出的由來!
執行“kv 1000”回溯大范圍的棧幀,你可以看到 683 次(棧幀編號 0x2b0 - 5)對 tail_recursivef_factorial() 的調用,在
我們的預測點(685 號棧幀)之前就發生了溢出:
最后介紹一個強大的命令“!analyze -v”,它會自動分析內核崩潰的原因,並給出所有對故障排除有幫助的信息,對於本例而言,
有價值的信息截圖如下:
—————————————————————————————————————————————————————
小結:本篇以源碼+調試+在線文檔等綜合手段排除了“double fault”藍屏故障;編寫驅動並上機調試是理解內核設計思想
的最佳途經!
—————————————————————————————————————————————————————