Windbg在软件调试中的应用

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:WINDOWSsystem32目录。

3.我们自己开发的应用程序的符号文件路径。

至此我们已经完成了准备工作。如果说,选择题,填空题,简答题是考试时常遇到的题型, 那么让我们来看看,在软件调试过程中常遇到的题型如何解吧。

现在介绍如何利用Windbg进行软件异常,死锁,内存泄漏等软件问题的排查方法。

  1. 常见的软件崩溃主要是断言与未处理异常,断言引起的软件崩溃相对来说比较容易定位,而未处理异常的定位比较困难,现在看看如何利用Windbg进行未处理异常错误的排查,其步聚如下:

    1. 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

    2. 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

    3. 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

    4. 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: estJustTestJustTestDlg.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

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

       

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

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

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

    1. 例如一个示例程序:

      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: estJustTestJustTestDlg.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: estJustTestJustTestDlg.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的第一个参数看出来)

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

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

      以JustTest.exe 与 JustTest0.exe 为例

    3. 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所占有

    4. 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 命令显示的线程信息中,只会显示该线程在等待的内核对象的信息,而不会显示该线程占有的的内核对象的信息

  3. 从以上的介绍中,你会发现对于程序异常,死锁现象都比较容易定位,那么对于内存泄漏这种情况又如何呢?同样,排查方法也是比较简单

    现在介绍一下如何利用windbg提供的工具来排查内存泄漏问题.

    首先需要设置PDB文件的路径, 设置环境变量:变量名为_NT_SYMBOL_PATH, 变量值为PDB文件所在的路径我的设置是:C:WINDOWSSymbols;C:WINDOWSsystem32;E:Projectpdb

    修改注册表使操作系统对每一次内存分配操作时的调用栈进行记录:

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

    总之一切源于思考。

     

     

     

原文地址:https://www.cnblogs.com/MakeView660/p/9684807.html