一. 准備工作
這里一個有關鍵區鎖死問題的程序,運行之后依次點擊“CS鎖死”按鈕、右上角退出按鈕,程序就會卡死。(圖1)
對於眼下的這個問題,界面完全失去響應,這說明負責消息處理的UI線程阻塞了。對於幾乎所有的windows GUI程序,編號為0的初始線程就是UI線程,windows發現該界面一段時間沒有消息響應之后就會在標題后面加上“(未響應)”。
二. 開始調試
啟動Windbg,附加到執行進程(F6),這時如果在windbg輸出的上面看到如下內容(圖2),說明第一步的環境變量設置生效了。
~*knv3
查看各個線程的調用堆棧(圖3),數字3表示顯示的堆棧深度,省略即顯示完整堆棧。
#0號線的棧幀0表示線程程阻塞在NtWaitForSingleObject
函數,MSDN得知該函數原型為:
NTSTATUS WINAPI NtWaitForSingleObject(
_In_ HANDLE Handle,
_In_ BOOLEAN Alertable,
_In_ PLARGE_INTEGER Timeout
);
第一個參數Handle為其等待的句柄,第三個參數TimeOut為超時時間。
同樣從棧幀0得知NtWaitForSingleObject正在等待句柄000000c4,超時時間為0(即沒信號就一直等待)。
!handle 000000c4 f
命令查看該句柄的信息(圖4):
現在我們知道c4句柄就是線程20d0的句柄,主線程在退出的時候等待該線程退出,而該線程一直沒有退出,所以主線程卡死了。
根據圖3得知20d0線程就是#1線程,~1kvn
查看該線程完整堆棧(圖5):
棧幀00 NtWaitForSingleObject
表示線程在等待000000c0句柄。
!handle 000000c0 f
得知c0句柄為事件句柄:
0:002> !handle c0 f
Handle c0
Type Event
Attributes 0
GrantedAccess 0x100003:
Synch
QueryState,ModifyState
HandleCount 2
PointerCount 4
Name <none>
Object Specific Information
Event Type Auto Reset
Event is Waiting
!locks
查看進程中哪些鎖處於鎖定狀態(圖6):
從第一行結果可以得知是gcsName臨界區(需要有pdb才會顯示具體變量名)處於鎖定狀態。
其實,我們從棧幀02
RtlEnterCriticalSection
也可以很快的知道該線程一直在等待進入關鍵區。
經過分析,知道程序如法退出的原因了:線程#1中的關鍵區gcsName處於鎖定狀態(也就是一直等待進入關鍵區),導致線程#1阻塞無法執行。又因主線程在退出的時候執行了WaitForSingleObject等待#1線程,從而導致主線程卡死。
關鍵區機制主要是通過下面這樣的RTL_CRITICAL_SECTION結構來實現的,可以通過dt
命令查看該結構定義:
0:002> dt RTL_CRITICAL_SECTION
Test1!RTL_CRITICAL_SECTION
+0x000 DebugInfo : Ptr32 _RTL_CRITICAL_SECTION_DEBUG
+0x004 LockCount : Int4B
+0x008 RecursionCount : Int4B
+0x00c OwningThread : Ptr32 Void
+0x010 LockSemaphore : Ptr32 Void
+0x014 SpinCount : Uint4B
其中,LockCount字段用來標識關鍵區的鎖狀態,RecursionCount字段用來記錄遞歸次數,用來支持同一個線程多次進入關鍵區,OwningThread字段用來記錄進入(擁有)關鍵區的線程ID,LockSemaphore用來記錄這個關鍵區對應的事件對象,當有線程需要等待這個關鍵區時,便是通過等待這個事件來做到的,這個事件對象是按需創建的,如果LockSemaphore為NULL表示這個關鍵區從來沒有線程在此等待過。
通過圖6中的OwningThread=738得知,關鍵區被線程ID為738的線程所擁有,即Enter之后一直沒有Leave。
知道了是哪個線程獲取了關鍵區但沒有釋放,就可以很容易的在代碼中定位問題了。
!locks
沒有顯示LockSemaphore字段,我們可以通過!cs -l
命令獲取更為全面的關鍵區信息:
從上圖可以看到LockSemaphore=0xC0,正好是#1線程NtWaitForSingleObject的事件對象。