Windbg在軟件調試中的應用
Windbg是微軟提供的一款免費的,專門針對Windows應用程序的調試工具。借助於Windbg, 我們常見的軟件問題:軟件異常,死鎖,內存泄漏等,就可以進行高效的排查。
在開始用WinDbg調試應用程序之前,我們得先做些准備工作。
- 設置符號文件路徑。
- 設置源代碼路徑。
- 打開待調試的可執行程序或Dump文件。
上述3個操作步聚比較簡單,均在File菜單的子菜單項中設置,此處就不在細說,值得一提的就是需要設置的符號文件路徑有三類:
1.Windows自身的模塊的符號文件路徑(notepad.exe, ntdll.dll等),在http://www.microsoft.com/whdc/devtools/debugging/default.mspx有下載,注意對應當前電腦的操作系統版本。
2.MFC提供的一系列DLL對應的符號文件,一般情況下在C:\WINDOWS\system32目錄。
3.我們自己開發的應用程序的符號文件路徑。
至此我們已經完成了准備工作。如果說,選擇題,填空題,簡答題是考試時常遇到的題型, 那么讓我們來看看,在軟件調試過程中常遇到的題型如何解吧。
現在介紹如何利用Windbg進行軟件異常,死鎖,內存泄漏等軟件問題的排查方法。
-
軟件異常
常見的軟件崩潰主要是斷言與未處理異常,斷言引起的軟件崩潰相對來說比較容易定位,而未處理異常的定位比較困難,現在看看如何利用Windbg進行未處理異常錯誤的排查,其步聚如下:
-
利用windbg打開軟件出錯時抓取的Dump文件或直接利用Windbg進行Live Debug.
-
利用命令~*kb 顯示當前所有的線程調用棧
0:001> ~*kb
0 Id: 1524.1520 Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr Args to Child
0012d44c 7c92e9ab 7c8094f2 00000002 0012d478 ntdll!KiFastSystemCallRet
0012d450 7c8094f2 00000002 0012d478 00000001 ntdll!ZwWaitForMultipleObjects+0xc
0012d4ec 7c809c86 00000002 0012d61c 00000000 kernel32!WaitForMultipleObjectsEx+0x12c
0012d508 6976763c 00000002 0012d61c 00000000 kernel32!WaitForMultipleObjects+0x18
0012de9c 697682b1 0012f1d4 ffffffff 00198310 faultrep!StartDWException+0x5df
0012ef10 7c863059 0012f1d4 ffffffff 00415460 faultrep!ReportFault+0x533
0012f184 1021595d 0012f1d4 00402218 00400000 kernel32!UnhandledExceptionFilter+0x4cf
0012f1a8 00402187 c0000094 0012f1d4 10212843 MSVCRTD!_XcptFilter+0x3d [winxfltr.c @ 228]
0012ffc0 7c816d4f 7c99ce64 00000000 7ffdd000 JustTest!WinMainCRTStartup+0x1d7 [crtexe.c @ 45]
0012fff0 00000000 00401fb0 00000000 78746341 kernel32!BaseProcessStart+0x23
# 1 Id: 1524.14e4 Suspend: 1 Teb: 7ffde000 Unfrozen
ChildEBP RetAddr Args to Child
00c7ffc8 7c9707a8 00000005 00000004 00000001 ntdll!DbgBreakPoint
00c7fff4 00000000 00000000 00000000 00000000 ntdll!DbgUiRemoteBreakin+0x2d
-
觀察各線程調用棧,尋找函數UnhandledExceptionFilter
-
利用dd命令顯示UnhandledExceptionFilter第一次參數的內存值
0:001> dd 0012f1d4
0012f1d4 0012f2c8 0012f2dc 0012f200 7c9237bf
0012f1e4 0012f2c8 0012ffb0 0012f2dc 0012f29c
0012f1f4 0012f814 7c9237d8 0012ffb0 0012f2b0
0012f204 7c92378b 0012f2c8 0012ffb0 0012f2dc
0012f214 0012f29c 00402218 00000001 0012f2c8
0012f224 0012ffb0 7c957860 0012f2c8 0012ffb0
0012f234 0012f2dc 0012f29c 00402218 0012f600
0012f244 0012f2c8 00144c90 00230fd2 00000001
-
利用 .cxr 命令切換至發生未處理異常的線程的調用棧,其中 .cxr 的參數為剛顯示的內存的第二個數據。
0:001> .cxr 0012f2dc
eax=00000000 ebx=00000000 ecx=0012fe74 edx=00000000 esi=00144c90 edi=0012f600
eip=00401caf esp=0012f5a8 ebp=0012f600 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010216
JustTest!CJustTestDlg::OnButton1+0x2f:
00401caf f77df4 idiv eax,dword ptr [ebp-0Ch] ss:0023:0012f5f4=00000000
-
利用kbn命令顯示出錯線程的調用棧
0:001> kbn
*** Stack trace for last set context - .thread/.cxr resets it
# ChildEBP RetAddr Args to Child
00 0012f600 5f4398cc 0012f8a0 00144c90 00000000 JustTest!CJustTestDlg::OnButton1+0x2f [D:\test\JustTest\JustTestDlg.cpp @ 179]
01 0012f638 5f439ffb 0012fe74 000003e8 00000000 MFC42D!_AfxDispatchCmdMsg+0xa2 [cmdtarg.cpp @ 88]
02 0012f690 5f435c1b 000003e8 00000000 00000000 MFC42D!CCmdTarget::OnCmdMsg+0x274 [cmdtarg.cpp @ 302]
03 0012f6c0 5f431f33 000003e8 00000000 00000000 MFC42D!CDialog::OnCmdMsg+0x24 [dlgcore.cpp @ 97]
04 0012f720 5f431135 000003e8 00230fd2 0012f8a0 MFC42D!CWnd::OnCommand+0x138 [wincore.cpp @ 2099]
05 0012f820 5f4310b8 00000111 000003e8 00230fd2 MFC42D!CWnd::OnWndMsg+0x53 [wincore.cpp @ 1608]
06 0012f840 5f42ec09 00000111 000003e8 00230fd2 MFC42D!CWnd::WindowProc+0x2e [wincore.cpp @ 1596]
07 0012f8b4 5f42f0f5 0012fe74 001b0f56 00000111 MFC42D!AfxCallWndProc+0xed [wincore.cpp @ 215]
08 0012f8e0 5f49265d 001b0f56 00000111 000003e8 MFC42D!AfxWndProc+0xad [wincore.cpp @ 379]
09 0012f910 77d18709 001b0f56 00000111 000003e8 MFC42D!AfxWndProcBase+0x4a [afxstate.cpp @ 220]
0a 0012f93c 77d187eb 5f492613 001b0f56 00000111 USER32!InternalCallWinProc+0x28
0b 0012f9a4 77d1b368 00000000 5f492613 001b0f56 USER32!UserCallWinProcCheckWow+0x150
0c 0012f9f8 77d1b3b4 00668e50 00000111 000003e8 USER32!DispatchClientMessage+0xa3
0d 0012fa20 7c92eae3 0012fa30 00000018 00668e50 USER32!__fnDWORD+0x24
0e 0012fa44 77d194e3 77d1de6e 001b0f56 00000111 ntdll!KiUserCallbackDispatcher+0x13
0f 0012fa80 77d1b7ab 00668e50 00000111 000003e8 USER32!NtUserMessageCall+0xc
10 0012faa0 77d4fc9d 001b0f56 00000111 000003e8 USER32!SendMessageW+0x7f
11 0012fab8 77d46530 00669450 00000000 00669450 USER32!xxxButtonNotifyParent+0x41
12 0012fad4 77d28386 0014777c 00000001 00000000 USER32!xxxBNReleaseCapture+0xf8
13 0012fb58 77d2887a 00669450 00000202 00000000 USER32!ButtonWndProcWorker+0x6d5
-
至此已經找到引起未處理異常的線程及其調用棧,再結合相關的代碼,便能分析出BUG的原因。
同時,應注意,軟件崩潰無非由兩種原因引起:一:使用錯誤資料。二:傳入錯誤參數。在排查過程中應注意這兩個排查方向。
-
-
死鎖
有人說:多線程是萬惡的根源。確實,在多線程編程中有兩類 問題是不容易處理的。一:死鎖,二:共享資源保護。
但是對於死鎖,借助Windbg,還是相當比較容易排查的。
根據引起死鎖的原因, 常見的有臨界區死鎖與內核對象死鎖。
-
對於臨界區引起的死鎖,相對容易排查,在Windbg中可利用!cs命令,找出各線程占有的臨界區,再觀察其它線程是否在等待被占有的臨界區,以找出發生死鎖的根源。
例如一個示例程序:
0:000> ~*kb
. 0 Id: 1a04.109c Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr Args to Child
0012f510 7c92e9c0 7c93901b 00000780 00000000 ntdll!KiFastSystemCallRet
0012f514 7c93901b 00000780 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc
0012f59c 7c92104b 00416868 0040237d 00416868 ntdll!RtlpWaitForCriticalSection+0x132
0012f5a4 0040237d 00416868 0012f8a0 00144c90 ntdll!RtlEnterCriticalSection+0x46
0012f600 5f4398cc 0012f8a0 00144c90 00000000 JustTest!CJustTestDlg::OnButton1+0x6d [D:\test\JustTest\JustTestDlg.cpp @ 191]
1 Id: 1a04.1288 Suspend: 1 Teb: 7ffde000 Unfrozen
ChildEBP RetAddr Args to Child
00d5fef0 7c92d85c 7c8023ed 00000000 00d5ff24 ntdll!KiFastSystemCallRet
00d5fef4 7c8023ed 00000000 00d5ff24 00d5ffb4 ntdll!NtDelayExecution+0xc
00d5ff4c 7c802451 00002710 00000000 00d5ffb4 kernel32!SleepEx+0x61
00d5ff5c 00401ca9 00002710 74680000 746800e0 kernel32!Sleep+0xf
00d5ffb4 7c80b50b 00000000 74680000 746800e0 JustTest!WndProc+0x39 [D:\test\JustTest\JustTestDlg.cpp @ 179]
00d5ffec 00000000 00401087 00000000 00000000 kernel32!BaseThreadStart+0x37
該示例程序有兩個活動線程,但是此時這個示例程序對於用戶的任何操作均未響應,可以判斷這個程序死鎖了。
根據線程0中的函數RtlEnterCriticalSection,初步判斷該線程死鎖是由於臨界區引起。
因此利用!cs 命令找出當前被占有的臨界區及對應的線程調用棧。
0:000> !cs -l -o
-----------------------------------------
DebugInfo = 0x0014e8d8
Critical section = 0x00416868 (JustTest!g_cs+0x0)
LOCKED
LockCount = 0x1
OwningThread = 0x00001288
RecursionCount = 0x1
LockSemaphore = 0x780
SpinCount = 0x00000000
OwningThread DbgId = ~1s
OwningThread Stack =
ChildEBP RetAddr Args to Child
00d5fef0 7c92d85c 7c8023ed 00000000 00d5ff24 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
00d5fef4 7c8023ed 00000000 00d5ff24 00d5ffb4 ntdll!NtDelayExecution+0xc (FPO: [2,0,0])
00d5ff4c 7c802451 00002710 00000000 00d5ffb4 kernel32!SleepEx+0x61 (FPO: [Non-Fpo])
00d5ff5c 00401ca9 00002710 74680000 746800e0 kernel32!Sleep+0xf (FPO: [1,0,0])
00d5ffb4 7c80b50b 00000000 74680000 746800e0 JustTest!WndProc+0x39 (CONV: stdcall)
00d5ffec 00000000 00401087 00000000 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
從以上命令執行結果分析,線程1占有了臨界區0x00416868, 而此時線程1在Sleep, 線程0又在等待臨界區0x00416868(這一點可以從RtlEnterCriticalSection的第一個參數看出來)
由以上分析過程可見,臨界區引起的死鎖,排查是相對容易的。
-
然而由互斥體,事件等內核對象引起的死鎖排查步聚也是比較簡單的,需要使Windbg處於內核模式下。
假若現在有兩個示例程序,JustTest.exe 與 JustTest0.exe 它們之間處於互鎖狀態。現在看看如何利用Windbg對進程間程序互鎖的情況進行調試。
以JustTest.exe 與 JustTest0.exe 為例
-
運行Windbg,使其處於內核調試模式下。
-
確定發生互鎖的程序,輸入命令!process
lkd> !process 0 2 justtest0.exe
PROCESS 88db38b8 SessionId: 0 Cid: 02d8 Peb: 7ffd4000 ParentCid: 08e8
DirBase: 0a4c0c80 ObjectTable: e4941c40 HandleCount: 41.
Image: JustTest0.exe
THREAD 88ee8020 Cid 02d8.0b64 Teb: 7ffdf000 Win32Thread: e30cb008 WAIT: (UserRequest) UserMode Non-Alertable
88ed9b90 Mutant - owning thread 88d9a590
以上信息顯示進程 justtest0.exe的線程88ee8020在等待一個互斥體Mutant (88ed9b90), 該互斥體被線程88d9a590所占有
-
使用命令!Thread 顯示線程88d9a590的詳細信息
lkd> !thread 88d9a590
THREAD 88d9a590 Cid 0c84.0f1c Teb: 7ffde000 Win32Thread: e4e858e8 WAIT: (WrUserRequest) UserMode Non-Alertable
8900b5a8 SynchronizationEvent
Not impersonating
DeviceMap e1713878
Owning Process 0 Image: <Unknown>
Attached Process 88d9c8b0 Image: JustTest.exe
Wait Start TickCount 204830 Ticks: 24996 (0:00:06:30.562)
Context Switch Count 2836 LargeStack
UserTime 00:00:00.046
KernelTime 00:00:00.046
Win32 Start Address 0x004020d0
Start Address 0x7c810867
Stack Init a98a1000 Current a98a0c20 Base a98a1000 Limit a989b000 Call 0
Priority 10 BasePriority 8 PriorityDecrement 0 DecrementCount 16
Kernel stack not resident.
綜合以上內容分析 JustTest0.exe的一個線程88ee8020在等待一個互斥體Mutant (88ed9b90), 該互斥體被線程88d9a590所占有,而線程88d9a590又屬於進程 JustTest.exe。因此可使用windbg進入用戶態調試模式,很容易就查到當時這兩個線程88ee8020,88d9a590當前的調用棧。找到了死鎖的原因,解決也就相應的簡單了。
注意:使用以上方法調試內核對象引起的死鎖,需要注意的一點是!process 命令顯示的線程信息中,只會顯示該線程在等待的內核對象的信息,而不會顯示該線程占有的的內核對象的信息
-
-
內存泄漏
從以上的介紹中,你會發現對於程序異常,死鎖現象都比較容易定位,那么對於內存泄漏這種情況又如何呢?同樣,排查方法也是比較簡單
現在介紹一下如何利用windbg提供的工具來排查內存泄漏問題.
首先需要設置PDB文件的路徑, 設置環境變量:變量名為_NT_SYMBOL_PATH, 變量值為PDB文件所在的路徑我的設置是:C:\WINDOWS\Symbols;C:\WINDOWS\system32;E:\Project\pdb
修改注冊表使操作系統對每一次內存分配操作時的調用棧進行記錄:
gflags -i ProgramName +ust
C)利用UMDH工具抓取軟件在兩個時刻的調用記錄.
umdh -pn:ProgramName -f:firstTraceFile.txt
umdh -pn:ProgramName -f:secondTraceFile.txt
D)比較兩次抓取的調用記錄的差別
umdh firstTraceFile.txt secondTraceFile.txt > result.txt
E)分析result.txt, 判斷這段時間內導到內存泄漏的函數調用棧
以下是我的一個測試程序的結果:
測試過程是,先執行5次NEW操作,再執行3次delete操作
//
// Each log entry has the following syntax:
//
// + BYTES_DELTA (NEW_BYTES - OLD_BYTES) NEW_COUNT allocs BackTrace TRACEID
// + COUNT_DELTA (NEW_COUNT - OLD_COUNT) BackTrace TRACEID allocations
// ... stack trace ...
//
// where:
//
// BYTES_DELTA - increase in bytes between before and after log
// NEW_BYTES - bytes in after log
// OLD_BYTES - bytes in before log
// COUNT_DELTA - increase in allocations between before and after log
// NEW_COUNT - number of allocations in after log
// OLD_COUNT - number of allocations in before log
// TRACEID - decimal index of the stack trace in the trace database
// (can be used to search for allocation instances in the original
// UMDH logs).
//
2f70 ( 1fa0 - 4f10) 2 allocs BackTrace360
- 3 ( 2 - 5) BackTrace360 allocations
ntdll!RtlDebugAllocateHeap+000000E1
ntdll!RtlAllocateHeapSlowly+00000044
ntdll!RtlAllocateHeap+00000E64
MSVCRTD!_heap_alloc_base+0000013C (malloc.c, 200)
MSVCRTD!_heap_alloc_dbg+000001A2 (dbgheap.c, 378)
MSVCRTD!_nh_malloc_dbg+00000049 (dbgheap.c, 248)
MSVCRTD!_malloc_dbg+0000001F (dbgheap.c, 165)
MFC42D!operator new+00000024 (afxmem.cpp, 373)
MFC42D!operator new+00000016 (afxmem.cpp, 65)
te!CTeDlg::OnButton1+00000037 (E:\test\teDlg.cpp, 180)
MFC42D!_AfxDispatchCmdMsg+000000A2 (cmdtarg.cpp, 88)
MFC42D!CCmdTarget::OnCmdMsg+00000274 (cmdtarg.cpp, 302)
MFC42D!CDialog::OnCmdMsg+00000024 (dlgcore.cpp, 97)
MFC42D!CWnd::OnCommand+00000138 (wincore.cpp, 2099)
MFC42D!CWnd::OnWndMsg+00000053 (wincore.cpp, 1608)
MFC42D!CWnd::WindowProc+0000002E (wincore.cpp, 1596)
MFC42D!AfxCallWndProc+000000ED (wincore.cpp, 215)
通過以上線程調用棧可以很容易地查出引起內存泄漏的原因。
雖然使用Windbg能提高軟件調試效率,但關鍵還在於開發人員對於程序的了解程序。
總之一切源於思考。