调试相关的知识和工具

这一部分主要介绍用户态调试相关的知识和工具。包括:汇 编、异常(exception)、内存布局、堆(heap)、栈(stack)、CRT(C Runtime)、handle/Criticalsection/thread context/windbg/ dump/live debug和Dr Watson等。

书中不会对知识点作全面的介绍,而是针对知识点在调试中过程中应该如何使用进行说明。知识点本身在下面两本书中有非常详细的介绍:

Programming Applications for Microsoft Windows

Debugging Applications for Windows

2.1  排错的工具:调试器Windbg

本节介绍调试器Windbg的相关知识。Windbg的使用贯穿本书很多章节,它是分析问题的高效工具。

Windbg的下载地址是:

Install Debugging Tools for Windows 32-bit Version

http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx

建议安装到C:/Debuggers目录,后面的例子默认用这个目录。

开发人员写完代码,通常会在Visual Studio中按F5直接进行调试。使用Visual Studio自带调试器能够非常方便地在源代码上设定断点,检查程序的中间变量,单步骤执行。在完成代码阶段,Visual Studio自带的调试器能够非常方便地做源代码级别的排错。

Visual Studio调试器的典型用例是源代码级别的排错。与其相比,Windbg并不是一款针对特殊用例的调试器。Windbg提供了一个GUI界面,也可以在 源代码上直接用F5设定断点,但更多的情况下,调试人员会直接用文本的方式输入调试命令,Windbg执行对应的操作,用文本的方式返回对应的结果。 Windbg的调试命令覆盖了Windows平台提供的所有调试功能。

本节首先对调试器和符号文件作大致的介绍,然后针对常用的Windbg调试命令作演示。接下来介绍Windbg中强大而灵活的条件断点,最后介绍调试器目录下的相关工具。

对调试器深入的了解后,相信读者就能体会到Windbg和Visual Studio调试器设计上的区别,选用最合适的调试器来解决问题。

书中不会从Windbg的基本使用方法说起,而是着重介绍调试器原理,常用的命令,Windbg的高级用法和相关的工具。如果读者从来没有使用过Windbg,下面的文章可以提供帮助:

DebugInfo:
http://www.debuginfo.com/

Windows Debuggers: Part 1: A WinDbg Tutorial
http://www.codeproject.com/debug/windbg_part1.asp

2.1.1  调试器的功能:检查代码和资料,保存dump文件,控制程序的执行

调试器,无论是Visual Studio调试器还是Windbg,都是用来观察和控制目标进程的工具。对于用户态的进程,调试器可以查看用户态内存空间和寄存器上的资料。对于不同类 型的数据和代码,调试器能方便地把这些信息用特定的格式区分和显示出来。调试器还可以把一个目标进程某一时刻的所有信息写入一个文件(dump),直接打 开这个文件分析。调试器还可以通过设置断点的机制来控制目标程序什么时候停下来接受检查,什么时候继续运行。

关于调试器的工作原理,请参考Debugging Applications for Windows 这本书。

Windbg及其相关工具的下载地址:

http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx

在安装好Windbg后,可以在windbg.exe的主窗口按F1弹出帮助。这是了解和使用Windbg的最好文档。每个命令的详细说明,都可以在里面找到。

调试器可以直观地看到下面一些信息:

l  进程运行的状态和系统状态,比如进程运行了多少时间,环境变量是什么。

l  当前进程加载的所有EXE/DLL的详细信息。

l  某一个地址上的汇编指令。

l  查看内存地址的内容和属性,比如是否可写。

l  每个的call stack(需要symbol)。

l  Call stack上每个函数的局部变量。

l  格式化地显示程序中的数据结构(需要symbol)。

l  查看和修改内存地址上的资料或者寄存器上的资料。

l  部分操作系统管理的数据结构,比如Heap、Handle、CriticalSection等。

在Visual Studio调试器中,要查看上面的信息,需要在很多调试窗口中切换。而在Windbg中,只需要简单的命令就可以完成。

调试器的另外一个作用是设定条件断点。可以设定在某一个指令地址上停下来,也可以设定当某一个内存地址等于多少的时候停下来,或者当某一个exception/notification 发生的时候停下来。还可以进入一个函数调用的时候停下来,或跳出当前函数调用的时候停下来。停下来后可以让调试器自动运行某些命令,记录某些信息,然后让调试器自动判断某些条件来决定是否要继续运行。通过简单的条件断点功能,可以很方便地实现下面一些任务:

l  当某一个函数被调用的时候,在调试器输出窗口中打印出函数参数。

l  计算某一个变量被修改了多少次。

l  监视一个函数调用了哪些子函数,分别被调用了多少次。

l  每次抛C++异常的时候自动产生dump文件。

在Visual Studio调试器中也能够设定条件断点,但灵活性和功能远不能跟Windbg相比。

2.1.2  符号文件(Symbol file),把二进制和源代码对应起来

当用VC/VB编译生成EXE/DLL后,往往会同时生成PDB文件。PDB里面包含的是EXE/DLL的符号信息。

符号是指代码中使用到的类型和名字。比如下面这些都是符号包含的内容:

l  代码所定义的Class的名字,Class的所有成员的名字和所有成员的类型。

l  变量的名字和变量的类型。

l  函数的名字,函数所有参数的名字和类型,以及函数的返回值。

PDB文件除了包含符号外,还负责把符号和该符号所处的二进制地址联系起来。比如有一个全局变量叫做gBuffer ,PDB文件不仅仅记录了gBuffer 的类型,还能让调试器找到保存gBuffer 的内存地址。

有了符号文件,当在调试器中试图读取某一个内存地址的时 候,调试器会尝试在对应的PDB文件中配对,看这个内存地址是否有符号对应。如果能够找到,调试器就可以把对应的符号显示出来。这样,极大程度上方便了开 发人员的观察。对于操作系统EXE/DLL,微软也提供了对应的符号文件下载地址。

默认情况下,符号文件中包含了所有的结构、函数,以及对应的源代码信息。微软提供的Windows符号文件去掉了源代码信息、函数参数定义和一些内部数据结构的定义。

2.1.3  一个简单的上手程序

接下来用一个简单的例子演示一下Windbg的基本使用。下面这段代码的目的是把字符串"6969,3p3p" 中的所有3都修改为4。

        #include "stdafx.h"

        #include "stdlib.h"

 

        char* getcharBuffer()

        {

              return "6969,3p3p" ;

        }

 

        void changeto4p(char * buffer)

        {

              while(*buffer)

              {

                   if(*buffer == '3' )

                         *buffer='4' ;

                   buffer++;

              }    

        }

        int _tmain(int argc, _TCHAR* argv[])

        {

              printf("%s/n","Any key continue..." );

              getchar();

              char *str=getcharBuffer();

              changeto4p(str);

              printf("%s" ,str);

              return 0;

        }

这段代码会导致崩溃。崩溃后看到的接口如图2.1所示。

图2.1

接下来,一起用Windbg来看看上述对话框的具体含义是什么。

在启动Windbg调试以前,首先把程序对应的PDB文 件放到一个指定的文件夹。上面程序的EXE叫做crashscreen-shot.exe,把编译时候生成的crashscreen-shot.pdb文 件拷贝到C:/PDB文件夹。同时把程序的主CPP文件拷贝到C:/SRC文件夹。

接下来启动Windbg。像用Visual Studio调试程序一样,我们需要在调试器中运行对应的EXE。所以在Windbg的主窗口中,使用File→Open Executable菜单找到crashscreen-shot.exe,然后打开。

Windbg不会让目标进程立刻开始运行。相反,Windbg这时会停下来,让用户有机会对进程启动过程进行排错,或者进行一些准备工作,比如设定断点,如图2.2所示。

图2.2

上面的主窗口就是Windbg输出结果的地方。下面的0:000>提示符后面是用户输入命令的地方。输入命令g , 让程序继续运行,如图2.3所示。

图2.3

从程序的输出可以看到,程序已经开始运行了,在等待用户的输入。如果要用调试器让程序暂停接受检查,可以在Windbg中用Ctrl + Break快捷键,或者用Debug→Break命令完成,如图2.4所示。

图2.4

接下来让我们在getcharBufferchangeto4p 函 数上分别设定断点。要通过函数名设定断点,首先要让Windbg加载对应的PDB文件。通过Windbg的File→Symbol File Path菜单可以设定PDB文件的搜索路径。通过这个菜单我们把路径设定到C:/PDB。设定好了后,接下来可以用x命令找到程序中getcharBufferchangeto4p 函数的二进制入口地址。找到地址后,就可以用bp命令在这两个地址上设定断点了,如图2.5所示。

图2.5

设定好断点后,继续用g 命令恢复程序的执行,输入任一键后,会看到程序在getcharBuffe断点上停下,如图2.6所示。

图2.6

输入k命令,可以检查当前的callstack,如图2.7所示。

图2.7

从上面的输出可以看到,__tmainCRTStartup 函数调用了wmain 函数,然后wmain 函数调用了getcharBuf 函数。同时还可以看到每个函数的源代码路径,以及函数对应的行数。

Windbg也同时支持源代码级别的调试。通过File→Open菜单,打开放到C:/SRC中的源代码文件,看到Windbg UI变为如图2.8所示。

图2.8

左边窗口是对应的源代码,右边窗口是Windbg的输入和输出。由于已经通过了bp 命令在getcharBufferchangeto4p 函数上设定了断点,所以右边窗口对应函数前有红色小方块表示断点。

除了使用bp 命令,还可以直接在右边窗口中用鼠标定位光标,然后摁F5设定条件断点。如果要单步执行,可以像在Visual Studio里一样用F10或者F11。

接下来继续调试。首先输入bc 1 命令清除第二个断点,然后输入命令g 让程序继续运行,会发现调试器会停在如图2.9中所示的位置。

左边窗口中changeto4p 前的红色方块消失,原因是我们取消了第二个断点。左边*buffer=’4’ 反蓝显示,表示这一行是当前进程正在执行指令对应的源代码。

右边窗口中下面这3行表明了调试器暂停进程执行的原因是发生了Access Violation,就是访问违例错误:

(df8.9a0): Access violation - code c0000005 (first chance)

First chance exceptions are reported before any exception handling.

This exception may be expected and handled.

图2.9

下面的两行输出说明当前导致问题的指令位于004117f6地址上,是一个mov指令。该指令正把十六进制值36(就是4的ASCII)写入EAX寄存器指向的内存。该指令位于函数crashscreen_shot!changeto4p入口偏移0x36的地方。

crashscreen_shot!changeto4p+0x36:

004117f 6 c60034          mov     byte ptr [eax],34h         ds:0023:004157a5=33

该条指令其实就是导致崩溃的指令。指令位于地址0x004117f6上,跟崩溃时看到的错误截图中所描述的指令地址一样。

该指令试图写内存的时候发生了问题。用DC命令可以看目标内存(EAX)上的数据是什么(图2.10)。

dc命令的输出结果是:

0:000> dc eax

004157a 5  70337033 25000000 00000a73 00000000  3p3p...%s.......

004157b5  5f000000 6e005f00 74006100 76006900  ..._._.n.a.t.i.v

004157c5  5f006500 74007300 72006100 75007400  .e._.s.t.a.r.t.u

004157d5  5f007000 74007300 74006100 20006500  .p._.s.t.a.t.e.

004157e5  3d003d00 5f002000 69005f00 69006e00  .=.=. ._._.i.n.i

004157f5  69007400 6c006100 7a006900 64006500  .t.i.a.l.i.z.e.d

00415805  00000000 00000000 00000000 00000000  ................

00415815  53000000 6b636174 6f726120 20646e75  ...Stack around

图2.10

该输出的左边一列是内存地址,第一个地址是需要显示的起 始地址004157a5,接下来每一行地址的间距是4个DWORD。右边中间内存地址上的资料,用DWORD方式显示。最右边是这些数据对应的ASCII 字符。这里看到,eax指向的地址是004157a5,值是0x70337033,对应的ASCII资料是3p3p,跟程序设计相吻合。同时该内存地址也 跟崩溃时看到的错误界面中所描述的指令地址一样。

接下来!address命令的输出就有点让人沮丧了。 Windbg提示对应的Symbol没有设定正确。!address命令可以检查对应内存页的属性,而内存页的属性是操作系统维护的。所以这里需要加载操 作系统模块的PDB文件。在我们的C:/PDB目录中,并没有操作系统的PDB文件,所以命令无法正常执行。微软提供了操作系统对应的PDB文件的下载地 址,下面一篇文章介绍了设定的方法:

http://www.microsoft.com/whdc/devtools/debugging/debugstart.mspx

设定好了后,重新运行!address命令,得到正确的输出(图2.11)。

注意看,输出的结果是:

0:000> !address eax

    00400000 : 00415000 - 00002000

                    Type     01000000 MEM_IMAGE

                    Protect  00000002 PAGE_READONLY

                    State    00001000 MEM_COMMIT

                    Usage    RegionUsageImage

                    FullPath crashscreen-shot.exe

由于这块内存是只读的,所以mov指令会导致崩溃。

图2.11

Visual Studio调试器中能方便地检查局部变量的值。在Windbg中,可以通过x命令完成,或者通过View→Locals菜单打开局部变量窗口查看(图2.12)。

图2.12

从这个例子中可以看到,Windbg除了能用多种方式完 成基本的断点设定,单步执行,变量检查,检查内存资料,显示callstack外,还可以看到系统相关的更多信息,比如内存页的属性,这是Visual Studio做不到的。后面的例子会演示更多的Windbg命令,包括线程切换,显示DLL信息,反汇编,搜索内存,异常上下文恢复和复杂的条件断点。

至于程序的“69694p4p”为何在只读的存储器上,请参考:

What's wrong with you ? char *p

http://www.vchelp.net/itbookreview/view_paper.asp?paper_id=534

2.1.4  用Internet Explorer来操练调试器的基本命令

下面用Internet Explorer作为目标进程演示Windbg中更多的调试命令。

用Windbg来调试目标进程,有两种方法,分别是通过调试器启动,和用调试器直接监视(attach)正在运行的进程。

通过File→Open Executable菜单,可以选择对应的EXE在调试器中启动。通过File→Attach to a process可以选择一个正在运行的进程进行调试。

打开IE,访问www.msdn.com, 然后启动Windbg,按F6,选择刚刚启动的(最下面)iexplorer.exe进程。

IE的PDB文件也需要从微软的网站上下载。具体做法请参考上一节的链接。在我本地,我的symbol路径设定如下:

SRV*D:/websymbols*http://msdl.microsoft.com/download/symbols;D:/MyAppSymbol

这里的D:/websymbols目录是用来保存从msdl.microsoft.com上自动下载的操作系统符号文件。而我自己编译生成的符号文件,我都手动拷贝到D:/MyAppSymbol路径下。

接下来,在Windbg的命令窗口中(如果看不到可以用Alt+1打开),运行下面命令。

vertarget检查进程概况

vertarget命令显示当前进程的大致信息:

0:026> vertarget

Windows Server 2003 Version 3790 (Service Pack 1) MP (2 procs) Free x86 compatible

Product: Server, suite: Enterprise TerminalServer SingleUserTS

kernel32.dll version: 5.2.3790.1830 (srv03_sp1_rtm.050324-1447)

Debug session time: Thu Apr 27 13:53:50.414 2006 (GMT+8)

System Uptime: 15 days 1:59:13.255

Process Uptime: 0 days 0:07:34.508

  Kernel time: 0 days 0:00:01.109

  User time: 0 days 0:00:00.609

上面的0:026>是命令提示符,026表示当前的线程ID。后面会介绍切换线程的命令,到时候就可以看到提示符的变化。

跟大多数的命令输出一样,vertarget的输出非常明白直观,显示当前系统的版本和运行时间。

!peb 显示Process Environment Block

接着可以用!peb命令来显示Process Environment Block。由于输出太长,这里就省略了。

lmvm 检查模块的加载信息

用lmvm命令可以看任意一个DLL/EXE的详细信息,以及symbol的情况:

0:026> lmvm msvcrt

start    end        module name

77ba0000 77bfa000   msvcrt     (deferred)             

    Image path: C:/WINDOWS/system32/msvcrt.dll

    Image name: msvcrt.dll

    Timestamp:        Fri Mar 25 10:33:02 2005 (4243785E)

    CheckSum:         0006288A

    ImageSize:        0005A000

    File version:     7.0.3790.1830

    Product version:  6.1.8638.1830

    File flags:       0 (Mask 3F)

    File OS:          40004 NT Win32

    File type:        1.0 App

    File date:        00000000.00000000

    Translations:     0409.04b0

    CompanyName:      Microsoft Corporation

    ProductName:      Microsoft® Windows® Operating System

    InternalName:     msvcrt.dll

    OriginalFilename: msvcrt.dll

    ProductVersion:   7.0.3790.1830

    FileVersion:      7.0.3790.1830 (srv03_sp1_rtm.050324-1447)

    FileDescription:  Windows NT CRT DLL

    LegalCopyright:   © Microsoft Corporation. All rights reserved.

命令的第二行显示deferred,表示目前并没有加载msvcrt的symbol,可以用.reload命令来加载。在加载前,可以用!sym命令来打开symbol加载过程的详细输出:

.reload / !sym 加载符号文件

默认情况下,调试器不会加载所有的symbol文件。只 有某个调试器命令需要使用symbol的时候,调试器才在设定的符号文件路径中检查和加载。!sym命令可以让调试器在自动寻找symbol的时候给出详 细的信息,比如搜索和下载的路径。.reload命令可以让调试器加载指定模块的symbol。

0:026> !sym noisy

noisy mode - symbol prompts on

0:026> .reload /f msvcrt.dll

SYMSRV:  msvcrt.pd_ from http://msdl.microsoft.com/download/symbols: 80847 bytes copied        

DBGHELP: msvcrt - public symbols 

         c:/websymbols/msvcrt.pdb/62B8BDC3CC194D2992DCFAED78B621FC1/msvcrt.pdb

0:026> lmvm msvcrt

start    end        module name

77ba0000 77bfa000   msvcrt     (pdb symbols)          c:/websymbols/msvcrt.pdb/62B8BDC3CC194D2992DCFAED78B621FC1/msvcrt.pdb

    Loaded symbol image file: C:/WINDOWS/system32/msvcrt.dll

    Image path: C:/WINDOWS/system32/msvcrt.dll

    Image name: msvcrt.dll

    Timestamp:        Fri Mar 25 10:33:02 2005 (4243785E)

    CheckSum:         0006288A

    ImageSize:        0005A000

    File version:     7.0.3790.1830

    Product version:  6.1.8638.1830

    File flags:       0 (Mask 3F)

    File OS:          40004 NT Win32

    File type:        1.0 App

    File date:        00000000.00000000

    Translations:     0409.04b0

    CompanyName:      Microsoft Corporation

    ProductName:      Microsoft® Windows® Operating System

    InternalName:     msvcrt.dll

    OriginalFilename: msvcrt.dll

    ProductVersion:   7.0.3790.1830

    FileVersion:      7.0.3790.1830 (srv03_sp1_rtm.050324-1447)

    FileDescription:  Windows NT CRT DLL

    LegalCopyright:   © Microsoft Corporation. All rights reserved.

可以看到,symbol从msdl.microsoft.com自动下载后加载。

lmf 列出当前进程中加载的所有模块

lmf命令可以列出当前进程中加载的所有DLL文件和对应的路径:

0:018> lmf

start    end        module name

00d40000 00dda000   iexplore C:/Program Files/Internet Explorer/iexplore.exe

04320000 043c9000   atiumdva C:/Windows/system32/atiumdva.dll

10000000 1033d000   googletoolbar2 c:/program files/google/googletoolbar2.dll

37f00000 37f0f000   Cjktl32  E:/Program Files/Powerword 2003/Cjktl32.dll

r,d,e 寄存器,内存的检查和修改

r命令显示和修改寄存器上的值。

d命令显示内存地址上的值。

e命令修改内存地址上的值。

显示寄存器:

0:018> r

eax=7ffdc000 ebx=00000000 ecx=00000000 edx=7707f06d esi=00000000 edi=00000000

eip=77032ea8 esp=054efc14 ebp=054efc40 iopl=0         nv up ei pl zr na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246

ntdll!DbgBreakPoint:

77032ea8 cc              int     3

如果需要修改寄存器,比如把eax的值修改为0x0,可以用 r eax=0。

用d命令显示esp 寄存器指向的内存,默认为byte格式。

0:018> d esp

054efc14  a9 f0 07 77 e9 ef 4e 05-00 00 00 00 00 00 00 00  ...w..N.........

054efc24  00 00 00 00 18 fc 4e 05-00 00 00 00 7c fc 4e 05  ......N.....|.N.

054efc34  f2 8b ff 76 a1 f5 03 77-00 00 00 00 4c fc 4e 05  ...v...w....L.N.

054efc44  33 38 b4 75 00 00 00 00-8c fc 4e 05 bd a9 02 77  38.u......N....w

054efc54  00 00 00 00 25 ef 4e 05-00 00 00 00 00 00 00 00  ....%.N.........

054efc64  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

054efc74  58 fc 4e 05 00 00 00 00-ff ff ff ff f2 8b ff 76  X.N............v

054efc84  a1 e2 03 77 00 00 00 00-00 00 00 00 00 00 00 00  ...w............

用dd命令直接指定054efc14 地址,第二个d表示用DWORD格式。除了DWORD外,还有db(byte),du(Unicode),dc(char)等等。详细信息请参考帮助文档中d命令的说明。

0:018> dd 054efc14

054efc14  7707f0a9 054eefe9 00000000 00000000

054efc24  00000000 054efc18 00000000 054efc7c

054efc34  76ff8bf2 7703f5a1 00000000 054efc4c

054efc44  75b43833 00000000 054efc8c 7702a9bd

054efc54  00000000 054eef25 00000000 00000000

054efc64  00000000 00000000 00000000 00000000

054efc74  054efc58 00000000 ffffffff 76ff8bf2

054efc84  7703e2a1 00000000 00000000 00000000

e命令可以用来修改内存地址。跟d命令一样,e命令后面也可以跟类型后缀。比如ed命令表示用DWORD的方式修改。下面的命令把054efc14 地址上的值修改为11112222。

0:018> ed 054efc14   11112222

修改完成后,用dd命令检查054efc14 地址上的值。后面的 L4参数指定内存区间的长度长度为4个DWORD。这样输出就只有1行,而不是默认的8行。

0:018> dd 054efc14  L4

054efc14  11112222 40a15c00 00000000 40a15c00

有了上面几个命令,就可以访问和修改当前进程中的所有内存。这些命令的参数和格式非常灵活,详细内容参考帮助文档。

!address显示内存页信息

该命令在前面的例子中就用到过,可以显示某一个地址上的页信息:

0:001> !address 7ffde000

    7ffde000 : 7ffde000 - 00001000

                    Type     00020000 MEM_PRIVATE

                    Protect  00000004 PAGE_READWRITE

                    State    00001000 MEM_COMMIT

                    Usage    RegionUsagePeb

如果不带参数,可以显示更详细的统计信息。

S 搜索内存

S命令可以搜索内存。比如用在下面的地方:

4.         寻找内存泄漏的线索。比如知道当前内存泄漏的内容是一些固定的字符串,就可以在 DLL区域搜索这些字符串出现的地址,然后再搜索这些地址用到什么代码中,找出这些内存是从什么地方开始分配的。

5.         寻找错误代码的根源。比如知道当前程序返回了0x80074015这样的一个代码,但是不知道这个代码是由哪一个内层函数返回的,就可以在代码区搜索0x80074015,找到可能返回这个代码的函数。

下面就是访问sina.com的时候,用Windbg搜索ie里面www.sina.com.cn 的结果:

0:022> s -u 0012ff40 L?80000000 "www.sina.com.cn"

001342a0  0077 0077 0077 002e 0073 0069 006e 0061  w.w.w...s.i.n.a.

00134b82  0077 0077 0077 002e 0073 0069 006e 0061  w.w.w...s.i.n.a.

00134f2e  0077 0077 0077 002e 0073 0069 006e 0061  w.w.w...s.i.n.a.

0013570c  0077 0077 0077 002e 0073 0069 006e 0061  w.w.w...s.i.n.a.

结合S命令和前面介绍的修改内存命令,根本不需要用什么金山游侠就可以查找/修改游戏中主角的生命了 :-)

接下来,看看跟线程相关的命令。

!runaway 检查线程的CPU消耗

!runaway可以显示每一个线程所耗费usermode CPU时间的统计信息:

0:001> !runaway

 User Mode Time

  Thread       Time

   0:83c       0 days 0:00:00.406

  13:bd4       0 days 0:00:00.046

  10:ac8       0 days 0:00:00.046

  24:4f4       0 days 0:00:00.031

  11:d8c       0 days 0:00:00.015

  26:109c      0 days 0:00:00.000

  25:1284      0 days 0:00:00.000

  23:12cc      0 days 0:00:00.000

  22:16c0      0 days 0:00:00.000

  21:57c       0 days 0:00:00.000

  20:c00       0 days 0:00:00.000

  19:14e8      0 days 0:00:00.000

  18:1520      0 days 0:00:00.000

  16:9dc       0 days 0:00:00.000

  15:1654      0 days 0:00:00.000

  14:13f4      0 days 0:00:00.000

   9:104c      0 days 0:00:00.000

   8:1760      0 days 0:00:00.000

   7:cc8       0 days 0:00:00.000

   6:530       0 days 0:00:00.000

   5:324       0 days 0:00:00.000

   4:178c      0 days 0:00:00.000

   3:1428      0 days 0:00:00.000

   2:1530      0 days 0:00:00.000

   1:448       0 days 0:00:00.000

上面输出的第一列是线程编号和线程id。后一列对应的是该线程在用户态模式中的总繁忙时间。在该命令加上f参数,还可以看到内核态的繁忙时间。当进程内存占用率高的时候,通过该命令可以方便地找到对应的繁忙线程。

~ 切换目标线程

用~命令,可以显示线程信息和在不同线程之间切换:

0:001> ~

   0  Id: c0.83c Suspend: 1 Teb: 7ffdd000 Unfrozen

.  1  Id: c0.448 Suspend: 1 Teb: 7ffdb000 Unfrozen

   2  Id: c0.1530 Suspend: 1 Teb: 7ffda000 Unfrozen

   3  Id: c0.1428 Suspend: 1 Teb: 7ffd9000 Unfrozen

   4  Id: c0.178c Suspend: 1 Teb: 7ffd8000 Unfrozen

   5  Id: c0.324 Suspend: 1 Teb: 7ffdc000 Unfrozen

   6  Id: c0.530 Suspend: 1 Teb: 7ffd7000 Unfrozen

   7  Id: c0.cc8 Suspend: 1 Teb: 7ffd6000 Unfrozen

   8  Id: c0.1760 Suspend: 1 Teb: 7ffd5000 Unfrozen

   9  Id: c0.104c Suspend: 1 Teb: 7ffd4000 Unfrozen

  10  Id: c0.ac8 Suspend: 1 Teb: 7ffd3000 Unfrozen

  11  Id: c0.d8c Suspend: 1 Teb: 7ff9f000 Unfrozen

  13  Id: c0.bd4 Suspend: 1 Teb: 7ff9d000 Unfrozen

  14  Id: c0.13f4 Suspend: 1 Teb: 7ff9c000 Unfrozen

  15  Id: c0.1654 Suspend: 1 Teb: 7ff9b000 Unfrozen

  16  Id: c0.9dc Suspend: 1 Teb: 7ff9a000 Unfrozen

  18  Id: c0.1520 Suspend: 1 Teb: 7ff96000 Unfrozen

  19  Id: c0.14e8 Suspend: 1 Teb: 7ff99000 Unfrozen

  20  Id: c0.c00 Suspend: 1 Teb: 7ff97000 Unfrozen

  21  Id: c0.57c Suspend: 1 Teb: 7ff95000 Unfrozen

  22  Id: c0.16c0 Suspend: 1 Teb: 7ff94000 Unfrozen

  23  Id: c0.12cc Suspend: 1 Teb: 7ff93000 Unfrozen

  24  Id: c0.4f4 Suspend: 1 Teb: 7ff92000 Unfrozen

  25  Id: c0.1284 Suspend: 1 Teb: 7ff91000 Unfrozen

  26  Id: c0.109c Suspend: 1 Teb: 7ff90000 Unfrozen

0:001> ~0s

eax=0013e7c4 ebx=00000000 ecx=0013e7c4 edx=0000000b esi=001642e8 edi=00000000

eip=7c82ed54 esp=0013eb3c ebp=0013ed98 iopl=0         nv up ei pl nz na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202

ntdll!KiFastSystemCallRet:

7c82ed54 c3               ret

0:000>

上面的~0s命令,把当前线程切换到0号线程,也就是主线程。切换后提示符会变为0:000。

k,kb,kp,kv,kn 检查call stack

k命令显示当前线程的call stack。跟d命令一样,k后面可以跟很多后缀,比如kb、kp、kn、kv、kL等。这些后缀控制了显示的格式和信息量。具体信息请参考帮助文档和动手实践。

0:000> k

ChildEBP RetAddr 

0013eb38 7739d02f ntdll!KiFastSystemCallRet

0013ed98 75ecb30f USER32!NtUserWaitMessage+0xc

0013ee24 75ed7ce5 BROWSEUI!BrowserProtectedThreadProc+0x44

0013fea8 779ac61e BROWSEUI!SHOpenFolderWindow+0x22c

0013fec8 0040243d SHDOCVW!IEWinMain+0x129

0013ff1c 00402748 iexplore!WinMain+0x316

0013ffc0 77e523cd iexplore!WinMainCRTStartup+0x186

0013fff0 00000000 kernel32!BaseProcessStart+0x23

可以结合~和k命令,来显示所有线程的call stack. 输入~*k试一下。

u 反汇编

u命令把指定地址上的代码翻译成汇编输出。

0:000> u 7739d023

USER32!NtUserWaitMessage:

7739d023 b84a120000       mov     eax,0x124a

7739d028 ba0003fe7f       mov     edx,0x7ffe0300

7739d02d ff12             call    dword ptr [edx]

7739d02f c3               ret

如果符号文件加载正确,可以用uf命令直接反汇编整个函数,比如uf USER32! NtUserWaitMessage。

x 查找符号的二进制地址

有了符号文件,调试器就能查找源代码符号和该符号所处的二进制地址之间的映射。如果要找一个符号保存在什么二进制地址上,可以用x命令:

0:000> x msvcrt!printf

77bd27c2 msvcrt!printf = <no type information>

上面的命令找到了printf函数的入口地址在77bd27c2。

0:001> x ntdll!GlobalCounter

7c 99f 72c ntdll!GlobalCounter = <no type information>

上面的命令表示ntdll!GlobalCounter这个变量保存的地址是7c99f72c。

(注意: 符号对应的是变量和变量所在的地址,不是变量的值。上面的命令不是说ntdll!GlobalCounter这个变量的值是7c99f72c。要找到变量的值,需要用d命令读取内存地址来获取。)

x命令还支持通配符,比如 x ntdll!*命令列出ntdll模块中所有的符号,以及对应的二进制地址。由于输出太长,这里就省略了。

dds 对应二进制地址的符号

dds打印内存地址上的二进制值,同时自动搜索二进制值对应的符号。比如要看看当前stack 中保存了哪些函数地址,就可以检查ebp指向的内存:

0:000> dds ebp

0013ed98  0013ee24

0013ed9c  75ecb30f BROWSEUI!BrowserProtectedThreadProc+0x44

0013eda0  00163820

0013eda4  0013ee50

0013eda8  00163820

0013edac  00000000

0013edb0  0013ee10

0013edb4  75ece83a BROWSEUI!__delayLoadHelper2+0x23a

0013edb8  00000005

0013edbc  0013edcc

0013edc0  0013ee50

0013edc4  00163820

0013edc8  00000000

0013edcc  00000024

0013edd0  75f36d2c BROWSEUI!_DELAY_IMPORT_DESCRIPTOR_SHELL32

0013edd4  75f3a184 BROWSEUI!_imp__SHGetInstanceExplorer

0013edd8  75f36e80 BROWSEUI!_sz_SHELL32

0013eddc  00000001

0013ede0  75f3726a BROWSEUI!urlmon_NULL_THUNK_DATA_DLN+0x116

0013ede4  7c8d0000 SHELL32!_imp__RegCloseKey <PERF> (SHELL32+0x0)

0013ede8  7c925b34 SHELL32!SHGetInstanceExplorer

这里dds命令从ebp指向的内存地址0013ed98 开始打印。第1列是内存地址的值,第2列是地址上对应的二进制数据,第3列是二进制数据对应的符号。上面的命令自动找到了75ecb30f对应的符号是 BROWSEUI!BrowserProtectedThreadProc+0x44。

COM Interface和C++ Vtable里面的成员函数都是顺序排列的,所以dds命令可以方便地找到虚函数表中具体的函数地址。比如用下面的命令可以找到OpaqueDataInfo类型中虚函数对应的实际函数地址。

首先用x命令找到OpaqueDataInfo虚函数表地址:

0:000> x ole32!OpaqueDataInfo::`vftable'

7768265c ole32!OpaqueDataInfo::`vftable' = <no type information>

77682680 ole32!OpaqueDataInfo::`vftable' = <no type information>

接下来dds命令可以打印出虚函数表中的函数名字:

0:000> dds 7768265c

7768265c  77778245 ole32!ServerLocationInfo::QueryInterface

77682660  77778254 ole32!ScmRequestInfo::AddRef

77682664  77778263 ole32!ScmRequestInfo::Release

77682668  77779d26 ole32!OpaqueDataInfo::Serialize

7768266c  77779d3d ole32!OpaqueDataInfo::UnSerialize

77682670  77779d7a ole32!OpaqueDataInfo::GetSize

77682674  77779dcb ole32!OpaqueDataInfo::GetCLSID

77682678  77779deb ole32!OpaqueDataInfo::SetParent

7768267c  77779e18 ole32!OpaqueDataInfo::SerializableQueryInterface

77682680  777799b5 ole32!InstantiationInfo::QueryInterface

77682684  77689529 ole32!ServerLocationInfo::AddRef

77682688  776899cc ole32!ScmReplyInfo::Release

7768268c  77779bcd ole32!OpaqueDataInfo::AddOpaqueData

77682690  77779c43 ole32!OpaqueDataInfo::GetOpaqueData

77682694  77779c99 ole32!OpaqueDataInfo::DeleteOpaqueData

77682698  776a8cf6 ole32!ServerLocationInfo::GetRemoteServerName

7768269c  776aad96 ole32!OpaqueDataInfo::GetAllOpaqueData

776826a0  77777a3b ole32!CDdeObject::COleObjectImpl::GetClipboardData

776826a4  00000021

776826a8  77703159 ole32!CClassMoniker::QueryInterface

776826ac  77709b01 ole32!CErrorObject::AddRef

776826b0  776edaff ole32!CClassMoniker::Release

776826b4  776ec529 ole32!CClassMoniker::GetUnmarshalClass

776826b8  776ec546 ole32!CClassMoniker::GetMarshalSizeMax

776826bc  776ec589 ole32!CClassMoniker::MarshalInterface

776826c0  77702ca9 ole32!CClassMoniker::UnmarshalInterface

776826c4  776edbe1 ole32!CClassMoniker::ReleaseMarshalData

776826c8  776e5690 ole32!CDdeObject::COleItemContainerImpl::LockContainer

776826cc  7770313b ole32!CClassMoniker::QueryInterface

776826d0  7770314a ole32!CClassMoniker::AddRef

776826d4  776ec5a8 ole32!CClassMoniker::Release

776826d8  776ec4c6 ole32!CClassMoniker::GetComparisonData

2.1.5  检查程序资料的小例子

下面用一个小例子来演示如何具体地观察程序中的数据结构。

首先在debug模式下编译并且按Ctrl+F5运行下面的代码:

struct innner

{

   char arr[10];

};

class MyCls

{

private :

   char * str;

   innner inobj;

public :

   void set(char * input)

   {

        str=input;

        strcpy(inobj.arr,str);

   }

   int output()

   {

        printf(str);

        return 1;

   }

   void hold()

   {

        getchar();

   }

};

void foo1()

{

   MyCls *pcls=new MyCls();

   void *rawptr=pcls;

   pcls->set("abcd");

   pcls->output(); 

   pcls->hold();

};

void foo2()

{

   printf("in foo2/n"); 

   foo1();

};

void foo3()

{

   printf("in foo3/n"); 

   foo2();

};

int _tmain(int argc, _TCHAR* argv[])

{

   foo3();

   return 0;

}

当console等待输入的时候,启动Windbg,然后用F6加载目标进程。

用~0s命令切换到主线程,查看callstack:

0:000> knL

 # ChildEBP RetAddr 

00 0012f7a0 7c821c94 ntdll!KiFastSystemCallRet

01 0012f7a4 7c836066 ntdll!NtRequestWaitReplyPort+0xc

02 0012f7c4 77eaaba3 ntdll!CsrClientCallServer+0x8c

03 0012f8bc 77eaacb8 kernel32!ReadConsoleInternal+0x1b8

04 0012f944 77e41990 kernel32!ReadConsoleA+0x3b

05 0012f99c 10271754 kernel32!ReadFile+0x64

06 0012fa28 10271158 MSVCR80D!_read_nolock+0x584

07 0012fa74 10297791 MSVCR80D!_read+0x1a8

08 0012fa9c 102a029b MSVCR80D!_filbuf+0x111

09 0012faf0 102971ce MSVCR80D!getc+0x24b

0a 0012fafc 102971e8 MSVCR80D!_fgetchar+0xe

0b 0012fb04 0041163b MSVCR80D!getchar+0x8

0c 0012fbe4 00413f82 exceptioninject!MyCls::hold+0x2b

0d 0012fcec 0041169a exceptioninject!foo1+0xa2

0e 0012fdc0 004114fa exceptioninject!foo2+0x3a

0f 0012fe94 004116d3 exceptioninject!foo3+0x3a

10 0012ff68 00412016 exceptioninject!wmain+0x23

11 0012ffb8 00411e5d exceptioninject!__tmainCRTStartup+0x1a6

12 0012ffc0 77e523cd exceptioninject!wmainCRTStartup+0xd

13 0012fff0 00000000 kernel32!BaseProcessStart+0x23

.frame 在栈中切换以便检查局部变量

上面callstack中每一行前面的序号叫做frame number。通过.frame命令,可以切换到对应的函数中检查局部变量。比如exceptioninject!foo1 这个函数前面的 frame number是d,于是执行.frame d命令:

0:000> .frame d

0d 0012fcec 0041169a exceptioninject!foo1+0xa2 [d:/xiongli/today/exceptioninject/exceptioninject/exceptioninject.cpp @ 72]

x命令显示当前frame的局部变量。在foo1函数中,两个局部变量分别是pcls和rawptr:

0:000> x

0012fce4 pcls = 0x0039ba80

0012fcd8 rawptr = 0x0039ba80

dt 格式化显示资料

在符号文件加载的情况下,dt命令格式化显示变量的资料和结构:

0:000> dt pcls

Local var @ 0x12fce4 Type MyCls*

0x0039ba80

   +0x000 str              : 0x00416648  "abcd"

   +0x004 inobj            : inner

上面的命令打印出pcls的类型是MyCls指针,指向的地址是0x0039ba80,其中的两个class成员的偏移分别在+0和+4,对应的值在第2列显示。加上-b -r参数可以显示inner class和数组的信息:

0:000> dt pcls -b -r

Local var @ 0x12fce4 Type MyCls*

0x0039ba80

   +0x000 str              : 0x00416648  "abcd"

   +0x004 inobj            : innner

      +0x000 arr              :  "abcd"

       [00] 97 'a'

       [01] 98 'b'

       [02] 99 'c'

       [03] 100 'd'

       [04] 0 ''

       [05] 0 ''

       [06] 0 ''

       [07] 0 ''

       [08] 0 ''

       [09] 0 ''

对于任意的地址,也可以手动指定符号类型来格式化显示。比如把0x0039ba80地址上的数据用MyCls类型来显示:

0:000> dt 0x0039ba80 MyCls

   +0x000 str              : 0x00416648  "abcd"

   +0x004 inobj            : innner

2.1.6  用Windbg控制程序进行实时调试(Live Debug)

除了检查静态资料外,调试器还能够控制和观察代码的执行。

1.         wt命令

wt命令的作用是watch and trace data。 它可以跟踪一个函数的所有执行过程,并且给出统计信息。

2.         设定断点

Windbg里面可以设定灵活而强大的条件断点。比如可 以通过条件断点实现这样的功能:当某个全局变量被修改100次以后,同时stack上的第2个参数是100,那么就停下来进入调试模式;如果第2个参数是 200,那么就生成1个dump文件,否则就只打印出当前的callstack,然后继续运行。

Wt Watch and Trace, 跟踪执行的强大命令

还是对于上面那个程序。

首先用bp (break point) 命令在foo3上面设断点:

0:001> bp exceptioninject!foo3

breakpoint 0 redefined

然后用g命令让程序执行:

0:001> g

执行到foo3上的时候,调试器停下来了:

Breakpoint 0 hit

eax=0000000a ebx=7ffd7000 ecx=0043780e edx=10310bd0 esi=0012fe9c edi=0012ff68

eip=004114c0 esp=0012fe98 ebp=0012ff68 iopl=0         nv up ei pl zr na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246

exceptioninject!foo3

004114c0 55               push    ebp

用bd(breakpoint disable)命令取消设定好的断点,以免打扰wt的执行:

0:000> bd 0

用wt命令监视foo3的执行,深度设定成2(-l2参数):

0:000> wt -l2

Tracing exceptioninject!foo3 to return address 0041186a

   60     0 [  0] exceptioninject!foo3

   28     0 [  1]   MSVCR80D!printf

    5     0 [  2]     MSVCR80D!__iob_func

   32     5 [  1]   MSVCR80D!printf

   12     0 [  2]     MSVCR80D!_lock_file2

   35    17 [  1]   MSVCR80D!printf

    5     0 [  2]     MSVCR80D!__iob_func

   38    22 [  1]   MSVCR80D!printf

   50     0 [  2]     MSVCR80D!_stbuf

   46    72 [  1]   MSVCR80D!printf

    5     0 [  2]     MSVCR80D!__iob_func

   49    77 [  1]   MSVCR80D!printf

  575     0 [  2]     MSVCR80D!_output_l

   52   652 [  1]   MSVCR80D!printf

    5     0 [  2]     MSVCR80D!__iob_func

   57   657 [  1]   MSVCR80D!printf

   33     0 [  2]     MSVCR80D!_ftbuf

   60   690 [  1]   MSVCR80D!printf

    7     0 [  2]     MSVCR80D!printf

   71   697 [  1]   MSVCR80D!printf

   63   768 [  0] exceptioninject!foo3

    1     0 [  1]   exceptioninject!ILT+380(__RTC_CheckEsp)

    2     0 [  1]   exceptioninject!_RTC_CheckEsp

   64   771 [  0] exceptioninject!foo3

    1     0 [  1]   exceptioninject!ILT+340(?foo2YAXXZ)

   60     0 [  1]   exceptioninject!foo2

   71     0 [  2]     MSVCR80D!printf

   63    71 [  1]   exceptioninject!foo2

    1     0 [  2]     exceptioninject!ILT+380(__RTC_CheckEsp)

    2     0 [  2]     exceptioninject!_RTC_CheckEsp

   64    74 [  1]   exceptioninject!foo2

    1     0 [  2]     exceptioninject!ILT+215(?foo1YAXXZ)

  108     0 [  2]     exceptioninject!foo1

   70   183 [  1]   exceptioninject!foo2

    1     0 [  2]     exceptioninject!ILT+380(__RTC_CheckEsp)

    2     0 [  2]     exceptioninject!_RTC_CheckEsp

   73   186 [  1]   exceptioninject!foo2

   70  1031 [  0] exceptioninject!foo3

    1     0 [  1]   exceptioninject!ILT+380(__RTC_CheckEsp)

    2     0 [  1]   exceptioninject!_RTC_CheckEsp

   73  1034 [  0] exceptioninject!foo3

1107 instructions were executed in 1106 events (0 from other threads)

Function Name                     Invocations  MinInst  MaxInst AvgInst

MSVCR80D!__iob_func                         4        5        5       5

MSVCR80D!_ftbuf                              1       33       33      33

MSVCR80D!_lock_file2                        1       12       12      12

MSVCR80D!_output_l                           1      575      575     575

MSVCR80D!_stbuf                              1       50       50      50

MSVCR80D!printf                              3        7       71      49

exceptioninject!ILT+215(?foo1YAXXZ)       1        1        1       1

exceptioninject!ILT+340(?foo2YAXXZ)       1        1        1       1

exceptioninject!ILT+380(__RTC_CheckEsp)  4        1        1       1

exceptioninject!_RTC_CheckEsp              4        2        2       2

exceptioninject!foo1                         1      108      108     108

exceptioninject!foo2                         1      73      73     73

exceptioninject!foo3                         1      73      73     73

0 system calls were executed

eax=00000073 ebx=7ffd7000 ecx=00437c7e edx=10310bd0 esi=0012fe9c edi=0012ff68

eip=0041186a esp=0012fe9c ebp=0012ff68 iopl=0         nv up ei pl zr na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246

exceptioninject!wmain+0x4a:

0041186a ebe1             jmp     exceptioninject!wmain+0x2d (0041184d)

上面wt命令一直监视到foo3函数执行完为止。随着函数的执行,Windbg打印出foo3调用过的子函数。如果需要更详细的信息,可以调整深度参数的值。

wt命令最后给出统计信息。无论是观察函数执行过程和分支,或者是评估性能,wt命令都是很有帮助的。

断点和条件断点 (condition breakpoint),高效地控制观测目标

Windbg中的断点分为3种,命令格式和功能如下:

1.         bp+地址/函数名字可以在某个地址上设定断点。当程序运行到这个地址的时候断点触发。

2.         ba (break on access)用来设定访问断点,在某个地址被读/写的时候断点触发。

3.         Exception断点。当发生某个Exception/Notification的时候断点触发。详情请参考Windbg帮助中的sx(Set Exception)小结。

第1种格式前面已经实践过。第2种格式的断点也很简单。比如程序有一个全局变量,符号是testapp!g_Buffer。要想在程序修改这个变量的时候停下来,可以使用下面的命令设定断点:

ba w4 testapp!g_Buffer

上面的w4表示需要检查的类型和长度。W4中的W表 示类型为写(Write),4表示长度为4字节。testapp!g_Buffer是符号的名字,调试器会自动转换成该符号所在的内存地址。所以该断点的 作用是:监视一块内存地址区域,起点是testapp!g_Buffer所在的地址,长度为4字节。当有代码对该块地址任意位置有写操作发生的时候,调试 器就把程序断下来。

其实设置ba断点的原理很简单。在设置断点后,调试器通过API把所监视地址的页面属性改为不可访问。这样当有代码访问这块地址的时候,就会引起访问异常。这样调试器就可以监视内存的读写操作,作出相应判断。

第3种命令用来监视异常。调试器能捕获程序中所有的异 常,但是并不是说任何异常发生的时候调试器就一定要把程序断下来。调试人员可以通过sx命令来指定异常发生时候的对应操作。下面3条命令达到的效果是,当 Access Violation异常发生的时候,调试器就停下来。当DLL Load事件发生的时候,调试器就只是在屏幕上输出。当C++ exception发生的时候,调试器什么都不做。

Sxe av

Sxn ld

Sxd eh

关于异常的详细说明,在后面的小结有详细介绍。

条件断点(condition breakpoint)的是指在上面3种基本断点停下来后,执行一些自定义的判断。详细说明参考Windbg帮助中的Setting a Conditional Breakpoint小结。

在基本断点命令后加上自定义调试命令,可以让调试器在断点触发停下来后,执行调试器命令。每个命令之间用分号分割。

下面这个命令,在exceptioninject!foo3上设断点,每次断下来后,先用k显示callstack,然后用.echo命令输出简单的字符串‘breaks’,最后g命令继续执行:

0:001> bp exceptioninject!foo3 "k;.echo 'breaks';g"

breakpoint 0 redefined

0:001> g

ChildEBP RetAddr 

0012fe94 0041186a exceptioninject!foo3

0012ff68 00412016 exceptioninject!wmain+0x4a

0012ffb8 00411e5d exceptioninject!__tmainCRTStartup+0x1a6

0012ffc0 77e523cd exceptioninject!wmainCRTStartup+0xd

0012fff0 00000000 kernel32!BaseProcessStart+0x23

'breaks'

ChildEBP RetAddr 

0012fe94 0041186a exceptioninject!foo3

0012ff68 00412016 exceptioninject!wmain+0x4a

0012ffb8 00411e5d exceptioninject!__tmainCRTStartup+0x1a6

0012ffc0 77e523cd exceptioninject!wmainCRTStartup+0xd

0012fff0 00000000 kernel32!BaseProcessStart+0x23

'breaks'

 

更复杂一点的例子是:

int i=0;

int _tmain(int argc, _TCHAR* argv[])

{

  while (1)

  {

    getchar();

    i++;

    foo3();

  }

  return 0;

}

条件断点的命令是:

ba w4 exceptioninject!i "j (poi(exceptioninject!i)<0n40) '.printf /"exceptioninject!i value is:%d/",poi(exceptioninject!i);.echo;g';'.echo stop!'"

首先ba w4 exceptioninject!i表示在修改exceptioninject!i这个全局变量的时候停下来。j(judge)命令的作用是对后面的表达式作条件判断,如果为true,执行第1个单引号里面的命令,否则执行第2个单引号里面的命令。

条件表达式是 (poi“exceptioninject!i”<0n40)。在Windbg中,exceptioninject!i符号表示符号所在的内存地 址,而不是符号的数值,相当于C语言中的 &操作符的作用。Windbg命令poi的作用是取这个地址上的值,相当于C语言中的*操作符。所以这个条件的意思就是判断 exceptioninject!i的值,是否小于十进制(Windbg中十进制用0n当前缀)的40。

如果为真,那么就执行第1个单引号:

printf /"exceptioninject!i value is:%d/",poi(exceptioninject!i);.echo;g

这一个单引号里面有3个命令:.printf、.echo 和g,这里的printf语法跟C中printf函数语法一样。不过由于这个printf命令本身是在ba命令的双引号里面,所以需要用/来转义printf中的引号。转义的结果是:

printf “exceptioninject!i valus is %d”, poi(exceptioninject!i)

所以第1个单引号命令的作用是:

1)打印出当前exceptioninject!i的值;2).echo命令换行;3)g命令继续执行。

如果为假,那么就执行第2个单引号:.echo stop! 这个命令就是显示stop,由于后面没有g命令,所以windbg会停下。运行输出如下:

0:001> ba w4 exceptioninject!i "j (poi(exceptioninject!i)<0n40) '.printf /"exceptioninject!i value is:%d/",poi(exceptioninject!i);.echo;g';'.echo stop!'"

breakpoint 0 redefined

0:001> g

exceptioninject!i value is:35

exceptioninject!i value is:36

exceptioninject!i value is:37

exceptioninject!i value is:38

exceptioninject!i value is:39

stop!

eax=00000028 ebx=7ffd5000 ecx=5e186b9c edx=10310bd0 esi=0012fe9c edi=0012ff68

eip=00411872 esp=0012fe9c ebp=0012ff68 iopl=0         nv up ei pl nz na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206

exceptioninject!wmain+0x52:

00411872 e856f8ffff       call exceptioninject!ILT+200(?foo3YAXXZ) (004110cd)

伪寄存器,帮助保存调试的中间信息

考虑这样的情况,如果要记录某一个函数被执行了多少次,应该怎么做?简单的做法就是修改代码,在对应的函数入口做记录。可是,如果要记录的函数是系统API呢?

下面的命令可以统计VirtualAllocEx被执行了多少次:

bp kernel32!VirtualAllocEx "r $t0=@$t0+1;.printf /"function executes: %d times /",@$t0;.echo;g"

这里用到的$t0就是Windbg提供的伪寄存器。可以用来存储中间信息。这里用它来存储函数执行的次数。r命令可以用来查看,修改寄存器(CPU寄存器和Windbg的伪寄存器都有效)的值。随便挑一个繁忙的进程,用这个命令设定断点后观察:

0:009> bp kernel32!VirtualAllocEx "r $t0=@$t0+1;.printf /"function executes: %d times /",@$t0;.echo;g"

0:009> g

function executes: 1 times
function executes: 2 times

function executes: 3 times

function executes: 4 times

关于伪寄存器信息,可以参考帮助文档中的Pseudo-Register Syntax小结。

Step Out的实现

Step Out的定义是“Target executes until the current function is complete.”。Windbg中是如何实现这个功能的呢?根据这个定义,可以简单地在当前函数的返回地址上设定bp 断点就可以了。当前函数的返回地址保存在函数入口时候的EBP+4上。但如果简单地在EBP+4上面设定断点有两个问题:

1.         无法区分递归调用和函数返回,甚至其他线程对该地址的调用。

2.         第一次触发后不会自动清除端点,可能会多次触发。

如果观察windbg中step  out的实现,可以看到:

bp /1 /c @$csp @$ra;g

这里的/1参数使得断点在触发后自动清除,避免了第2个问题,/c @$csp参数通过指定callstack 的最小深度避免了第1个问题。而$ra伪寄存器直接表示当前函数的返回地址。多方便 :-)

2.1.7  远程调试(Remote debug)

远程调试是让调试人员远程地操作调试器的一种手段。

在调试WinForm程序的时候,如果要保持目标程序一直全屏运行,就没办法在同一台机器上切换到调试器输入命令。使用remote debug可以解决这样的情况,避免调试器对目标程序的干扰。

如果开发团队中的开发人员在两个城市,通过远程调试可以节省创建多个调试环境的时间。如果某些问题只能在固定的机器上重现,远程调试让排错过程简单便利。

在Windbg中,一种方法使用.server命令在本 地创建一个TCP端口或者通过named pipe,使得远程的Windbg可以连接到本地调试。双方都可以输入命令,执行结果在双方的Windbg上都显示出来。具体介绍参考Windbg中关 于.server命令的帮助。

另外一种更为强大的方法是使用DbgSrv。 DbgSrv是一个调试服务。跟.server命令不同的地方在于,.server只是简单地通过重定向方便远程调试人员检查,而实际的调试工作都发生在 目标机器上。DbgSrv则是让非常必要的调试动作发生在目标机器上,而次要的调试功能,比如加载PDB显示符号等,发生在调试人员的机器上。在 DbgSrv出现以前,调试系统的核心服务,比如lsass.exe进程,需要同时结合用户态调试器和内核调试器,而且符号文件必须位于目标机器上。 DbgSrv的出现让这个过程大为简化,请参考:

Debugging LSASS ... oh what fun, it is to ride..

http://blogs.msdn.com/spatdsg/archive/2005/12/27/507265.aspx

2.1.8  如何通过Windbg命令行让中文魔兽争霸运行在英文系统上

买了中文版的魔兽争霸,但家里的Windows却是英文版。中文的魔兽争霸必须要运行到中文的操作系统上,否则就报告操作系统语言不匹配。

为了解决这个问题,首先能想到的就是到Windows的地区设置里面去把国家改为中国。尝试后发现问题依旧。看来魔兽争霸判断的并非本地Local设置,而是操作系统的语言版本。怎么办呢?重装系统?去网上找破解?其实Windbg就可以解决问题。

获取系统语言版本的API是GetSystemDefaultUILanguage。所以可以在这个 API上设定条件断点,然后观察魔兽争霸判断语言版本的逻辑是怎样的。

用Windbg启动war3.exe,然后在 GetSystemDefaultUILanguage上设定断点。API触发后,发现调用完这个API后,war3.exe的下一条语句是一个cmp eax,ChineselanID,判断当前是否是中文系统。如果不是中文,就退出程序。

那好,在cmp这条语句上设定断点,然后用下面的命令把eax修改成中文语言符的 ID,就可以欺骗程序,让程序认为当前系统是中文:

r eax = 0n2052

eax被修改成了中文的语言符后,接下来的cmp执行结 果就跟中文系统上的一样了,war3就可以正确运行了。每次都要做这样的修改麻烦得很,为了简化这个过程,创建内容为如下的script文件,在 GetSystemDefaultUILanguage API返回前把ax设定为0n2052:

bp kernel32!GetSystemDefaultUILanguage+0x2c "r ax=0n2052;g"

每次启动魔兽争霸都要手动设定断点是很麻烦的事情。如果要简化整个过程,可以采用下面文章中介绍的方法,让war3.exe启动的时候自动启动Windbg,通过-cf参数自动执行我们的条件断点来达到欺骗war3的目的。

How to debug Windows services

http://support.microsoft.com/?kbid=824344

2.1.9  Dump文件

前面提到过,dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。当在调试器中打开dump文件时,使用前面介绍的命令检查,看到的结果跟用调试器检查进程是一样的。

在Windbg中可以通过.dump命令保存进程的dump文件。比如下面的命令把当前进程的镜像保存为c:/testdump.dmp文件:

.dump /ma C:/testdump.dmp

其中的/ma参数表示dump文件应该包含进程的完整信息,包括整个用户态的内存,这样dump文件尺寸会比较大,信息非常全面。如果不使用/ma参数,保存下来的dump文件只包含了部分重要资料,比如寄存器和线程栈空间,文件尺寸会比较小,无法分析所有的数据。

在Windbg中,通过File→Open Crash Dump菜单可以打开dump文件进行分析。打开dump文件后,运行调试命令看到的信息和状态,就是dump文件保存时进程的状态。通过dump文件能够方便地保存发生问题时进程的状态,方便事后分析。

2.1.10  CDB、NTSD和重定向到Kernel Debugging

除了Windbg,另外有两个调试器分别叫做CDB和NTSD。Windbg、CDB和NTSD三者使用的命令都完全一样。只是Windbg提供了窗口接口,剩下两个是基于命令行的工具。NTSD位于system32目录下,不需要特别安装。

这3个工具其实都使用了同样的调试引擎dbgeng.dll。关于调试引擎的详细信息,请参考:

Symbols and Crash Dumps

http://msdn.microsoft.com/msdnmag/issues/02/06/Bugslayer/

由于CDB和NTSD采用命令行标准输入输出,所以可以 很方便地通过重定向来控制这两个工具。一个典型的用例就是可以把用户态的调试重定向到Kernel Debugger。这样只需要一个Debugging Session就可以同时控制核心态和用户态的调试例程。详细信息请参考Windbg 帮助中的CDB and NTSD小结。

2.1.11  Debugger Extension,扩展Windbg的功能

Debugger Extension相当于是用户自定义,可编程的Windbg插件。一个最有用的extension就是.NET Framework 提供的sos.dll。它可以用来检查.NET程序中的内存、线程、callstack、appdomain、assembly等等信息。关于 sos.dll后面会作详细讲解。关于如何开发自己的Debugger Extension,可以参考:

Debug Tutorial Part 4: Writing WINDBG Extensions

http://www.codeproject.com/debug/cdbntsd4.asp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值