Windbg在软件调试中的应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/G66565906/article/details/58591311

Windbg在软件调试中的应用

                                                        

Windbg是微软提供的一款免费的,专门针对Windows应用程序的调试工具。借助于Windbg, 我们常见的软件问题:软件异常,死锁,内存泄漏等,就可以进行高效的排查。

在开始用WinDbg调试应用程序之前,我们得先做些准备工作。

1.设置符号文件路径。

2.设置源代码路径。

3.打开待调试的可执行程序或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进行未处理异常错误的排查,其步聚如下:

1.利用windbg打开软件出错时抓取的Dump文件或直接利用Windbg进行Live Debug.

2.利用命令~*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

3.观察各线程调用栈,寻找函数UnhandledExceptionFilter

4.利用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

5.利用 .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

6.利用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

7.至此已经找到引起未处理异常的线程及其调用栈,再结合相关的代码,便能分析出BUG的原因。

同时,应注意,软件崩溃无非由两种原因引起:一:使用错误资料。二:传入错误参数。在排查过程中应注意这两个排查方向。

二.死锁

有人说:多线程是万恶的根源。确实,在多线程编程中有两类       问题是不容易处理的。一:死锁,二:共享资源保护。

但是对于死锁,借助Windbg,还是相当比较容易排查的。

根据引起死锁的原因, 常见的有临界区死锁与内核对象死锁。

A) 对于临界区引起的死锁,相对容易排查,在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, 而此时线程1Sleep, 线程0又在等待临界区0x00416868(这一点可以从RtlEnterCriticalSection的第一个参数看出来)

由以上分析过程可见,临界区引起的死锁,排查是相对容易的。

B)然而由互斥体,事件等内核对象引起的死锁排查步聚也是比较简单的,需要使Windbg处于内核模式下。

假若现在有两个示例程序,JustTest.exe 与 JustTest0.exe 它们之间处于互锁状态。现在看看如何利用Windbg对进程间程序互锁的情况进行调试。

JustTest.exe 与 JustTest0.exe 为例

1.运行Windbg,使其处于内核调试模式下。

2.确定发生互锁的程序,输入命令!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所占有

3.使用命令!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进入用户态调试模式,很容易就查到当时这两个线程88ee802088d9a590当前的调用栈。找到了死锁的原因,解决也就相应的简单了。

注意:使用以上方法调试内核对象引起的死锁,需要注意的一点是!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, 判断这段时间内导到内存泄漏的函数调用栈

以下是我的一个测试程序的结果:

 测试过程是,先执行5NEW操作,再执行3delete操作  

//                                                                          

// 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能提高软件调试效率,但关键还在于开发人员对于程序的了解程序。

总之一切源于思考。

猜你喜欢

转载自blog.csdn.net/G66565906/article/details/58591311