`
923723914
  • 浏览: 635892 次
文章分类
社区版块
存档分类
最新评论

windows程序员进阶系列:《软件调试》之O--- WinDbg使用介绍

 
阅读更多

windows程序员进阶系列:《软件调试》之O--- WinDbg使用介绍

拥有一个顺手的武器是每一个武林高手梦寐以求的。对于windows程序员来说,WinDbg调试器就是我们的武器。熟练使用调试器能大大提高我们的调试能力。本博文将详细介绍涉及到WinDbg调试器的基本使用方法以及在实际调试过程中经常使用到得各种命令和技巧。

WinDbg是一个功能非常强大的调试器。它支持多种调试任务,如用户态调试、内核态调试、转储文件调试和远程调试。与此同时WinDbg还具有很好的灵活性和可扩展性。虽然是一个典型的GUI应用程序,但是它的大多数调试功能还是以手工输入命令的方式来工作的。目前版本的WinDbg提供了130多条标准命令,140多条元命令和难以计数的扩展命令。

WinDbg使用工作空间来描述和存储调试项目的属性、参数及调试器设置等信息。工作空间与vc中的项目文件很相似。WinDbg定义了两种工作空间,一种为默认工作空间,另一种为命名的工作空间。当没有明确使用某个命名空间时,WinDbg总是使用默认工作空间。

WinDbg在安装后就有预先创建了一些列默认空间。分别为基础工作空间、默认内核工作空间、默认远程调试工作空间、特定处理器工作空间、默认用户态工作空间。它们分别定义了在WinDbg在各种条件下的一些配置、参数设置等。

基础工作空间:当调试会话尚未建立,WinDbg处于闲置状态时,会使用此空间作为默认空间。

默认内核工作空间:当WinDbg开始内核调试,但是尚未与调试目标建立连接时,会使用此空间作为默认空间。

默认远程调试工作空间:当通过调试服务器进行远程调试时,会使用此空间作为默认空间。

默认的用户态工作空间:当使用WinDbg调试一个已运行的进程时,会使用这个空间作为默认工作空间。

WinDbg打开一个应用程序开始调试时,调试器会根据可执行文件的路径和文件名为其创建一个默认工作空间,如果已经存在工作空间,就使用已存在的。在WinDbg的文件菜单中可以使用另存为..创建一个命名的工作空间。

WinDbg的工作空间中保存了一下信息:

调试会话状态:包括,断点、打开的源文件、用户自定义别名。

调试器设置:符号文件、可执行文件路径、源文件路径等。

WinDbg图形界面信息:包括WinDbg窗口标题、默认字体、窗口在桌面的位置、打开的子窗口、以及每个子窗口的状态等。

WinDbg默认使用注册表来保存工作空间设置,其路径为:HKEY_CURRENT_USER/Software/Microsoft/WinDBG/Workspaces

在此路径下有四个键:UserKernelDumpExplicit。前三个键分别用来保存用户态调试、内核态调试、调试转储文件时使用的默认空间。Explicit用以存储命名的工作空间。在四个键下的每个键对应于一个工作空间。键值名为工作空间的名称,键值就是这个工作空间的配置数据。

WinDbg支持使用文件来保存工作空间。这可以使用SaveworkspacetoFile

使用DeleteworkSpaces可以删除工作空间。更快的方法是直接删除保存在注册表中的键值。

WinDbg命令

WinDbg的大多数功能是使用命令方式工作的。命令分为三种:标准命令、元命令和扩展命令。

标准命令:提供适用于所有调试目标的基本调试功能。

标准命令在调试器内部实现,执行时不需要加载任何扩展模块。标准命令第一个字符不区分大小写,后面的字符可能区分大小写。共有130多条命令,分为60多个系列18个子类:

一:控制调试目标执行,包括恢复运行的g系列命令。跟踪执行的t系列命令。单步执行的p系列命令和跟踪监视的wt命令。

二:观察和修改寄存器的r系列命令。

三:读写IO端口命令。

四:观察、修改和搜索内存数据的d系列、e系列和s命令。

五:观察栈的k系列命令。

六:显示进程的|(非l)命令。

七:显示和控制线程的~命令。

八:设置和维护断点的bp(软件断点),ba(硬件断点)和管理断点的bl(列出所有断点),bc,bd,be(清除,禁用和重新启用断点)命令。

九:评估表达式的?命令和评估C++表达式的??命令。

十:用于汇编的a命令和反汇编的u命令。

十一:显示段选择子的dg命令。

十二:执行命令文件的$命令。

十三:设置调试事件处理方式的sx系列命令,启用与禁止静默模式的sq命令,设置内核选项的so命令,设置符号后缀的ss命令。

十四:显示调试器和调试目标版本的version命令。显示调试目标所在系统信息的vertarget命令。

十五:检查符号的x命令。

十六:控制和显示源程序的ls系列命令。

十七:加载调试符号的ld系列命令。搜索相邻符号的ln命令。显示模块列表的lm命令。

十八:结束调试会话的q命令。

在命令编辑框中输入?,可以显示主要标准命令和每个命令的简单介绍。

元命令

元命令用于提供标准命令没有提供的常用调试命令,元命令也是内建在调试器引擎或WinDbg程序文件中。所有元命令都是以.开始,所以元命令也被称为点命令。

按照功能,元命令可以分为一下几类:

一:显示和设置调试器会话和调试器选项,如符号选项.symopt,用于符号号路径的.sympath.symfix。用于源程序文件的.srcpath.srcnoise.srcfix。用于扩展命令模块的路径的.extpath。用于匹配扩展命令的.extmatch。用于可执行文件的.exepath。设置反汇编选项的.asm。控制表达式评估器的.expr命令。

二:控制调试会话或调试目标。如重新开始的.restart命令。放弃用户态调试目标的.abandon。创建新进程的.create命令和附着在存在进程的.attach命令。打开转储文件的.opendump。分离调试目标的.detach。用于杀掉进程的.kill

三:管理扩展命令模块。如加载模块的.load命令。卸载模块的.unloead命令和.unloadall命令。显示已加载模块的.chain命令。

四:管理调试器日志文件。如.logfile(显示信息).logopen(打开文件)。.logappend(追加),和.logclose(关闭文件)。

五:远程调试命令。

六:控制调试器。如让调试器睡眠一段时间的.sleep命令。唤醒调试器的.wake命令。启动另一个调试器来调试当前调试器的.dbgdgb命令。

七:编写命令程序。包括一系列类似于C语言关键字的命令。如.if.else.elseif.foreach.do.while.continue.catch.break.leave等。

八:显示或转储调试目标数据。如产生转储文件的.dump命令。将原始数据写到文件的.writemem命令。显示调试会话事件的.time命令。显示线程时间的.ttime命令。显示任务列表的.tlist命令。以不同格式显示数字的.format命令。

输入.help命令可以列出所有元命令和每个元命令的简单说明。

扩展命令

扩展命令用于实现针对特定目标的调试功能。与标准命令和元命令内建在Windbg程序文件中不同,扩展命令是是现在动态加载的扩展模块dll中。利用WinDbgsdk,用户可以自己编写扩展模块和扩展命令。

执行扩展命令时应该以感叹号!开始。!在英语中为bang。因此扩展命令也被称为bangcommand

执行扩展命令的完整格式:

nameofExtentModule.nameofExtentCommand参数。

其中nameofExternModule可以省略。省略后,WinDbg会在已加载的扩展模块中搜索指定的命令。如果此模块没有加载,则将模块加载。

因为扩展命令是在扩展模块中实现的,所以执行时需要加载相应的模块。WinDbg可以根据调试目标的类型和当前的工作空间自动加载命令空间指定的扩展模块。用户也可以调用.load命令手动加载扩展模块。.load后跟模块路径。

除了.load之外,还可以使用.loadby命令加上扩展模块名称和一个已加载的程序模块的名称。这时,WinDbg会在已加载模块所在目录搜索扩展模块。

大多数扩展模块都支持使用help命令来显示这个模块的基本信息和所包含命令。如:!ext.help

用户界面

命令窗口是用户与WinDbg交互的最主要的窗口。它有上下两部分组成。上面是命令显示区,下面是命令横条。命令行条又分为两部分。左边是命令提示符,右面是命令编辑框。

信息显示区是WinDbg输出各种调试信息的主要场所,包括命令执行的结果、调试事件、错误信息和调试引擎的提示信息。

WinDbg的命令提示符由文字和大于号组成,对于不同类型的调试目标和调试会话状态,命令提示符会有不同。

WinDbg启动后尚未与任何调试目标,建立调试对话处于待用状态时,它的提示符区域不显示任何内容。命令提示符显示尚未与调试目标建立连接。

WinDbg处于命令模式等待用户输入时,它的提示符是描述当前调试目标的简短文字加一个大于号。对于用户态目标,命令的提示符的完整格式是:

[||system_index]<process_index:thread_index>

sytem_index代表系统序号(有时会不显示),同一个Windows系统中的多个用户态目标属于每个系统,每个单独的内核目标单独属于一个系统。process_index代表进程序号。thread_index代表线程序号。对于多个进程的调试,所有序号共享一套序号,都是从0开始全局排的。

对于上图,仅仅显示了进程序号和线程序号。并没有显示系统序号。

在状态条也会显示系统序号、进程号和线程号。

设置符号路径

符号路径可以有多个,中间用分号分隔。如:E:\windbgSymbol;D:\symbol;

windbg有一个强大的功能,可以自动到Microsoft的服务器上下载符号文件。但是需要在符号路径下做一下设置:

其中:E:\windgbsymbol为本地目录,从服务器下载的符号会存储在此目录中。http://msdl.microsoft.com/download/symbols为服务器路径。如果相关符号表没有在本地目录系找到的话,就会自动在指定的服务器下载。

可以使用图形界面设置符号表路径。也可以使用.sympath命令来设置。对于上面的路径可以这样设置:

然后再输入:

将本地目录加入到符号搜索路径。

设置符号路径完成后需要使用.reload命令重新载入符号。

表达式格式

WinDbg支持C++masm两种表达格式。@@用于改变语法格式。在masm下使用@@会切换到C++语法格式。反之亦然。默认语法为masm语法格式。数制表示:0x表示16进制,0n表示十进制,0t表示8进制,0y表示二进制。默认为16进制。.expr指令用于设置表达式语法解释器。

经常使用的MASM表达式如:poi命令。该命令可以从指定地址得到指针长度的数据。

Windbg支持两种注释命令。一种是*命令,该命令会将其后的所有内容都当做注释。另一种是$$命令后的注释会以分号结束。

可以使用Ctrl+Break来终止一个长时间未完成的命令。

伪寄存器

为了方便的引用被调试程序的数据和,windbg定义了一些列的伪寄存器。可以在命令编辑框中直接使用伪寄存器,WinDbg调试引擎会自动将伪寄存器替换为为合适的值。比较常用的伪寄存器如下:

$ea上一条指令的有效地址

$ra当前函数的返回地址

$ip指令指针

$exentry当前进程的入口地址。

$proc当前进程EPROCESS结构的地址。

$thread当前进程的ETHREAD结构地址。

$peb当前进程的环境控制块。

$teb当前进程的线程环境块。

$tpid当前进程ID

$tid当前线程ID

$frame当前帧序号。

除了上面列出的伪寄存器,WinDbg还为用户准备了20个伪寄存器。它们的名称是$t0-$t19。用户可以使用这些寄存器值来保存任意的整数值。它们的初始值都是0,可以使用r命令来设置新的取值。

条件执行命令

.if.else.elseif元命令可以用于条件执行控制。

如:.if(ecx>2){recx}.else{reax}

循环命令

循环执行可以使用z命令。Z命令会循环执行它前面的命令,然后测试自己的条件。如:recx;recx=ecx-1;z(ecx);

循环执行命令还可以使用!for_each_XXX扩展命令。如!for_each_frame命令可以对每个栈帧执行一个操作。!for_each_local是针对每个局部变量。

进程和线程限定符

|为进程限定符。

|.表示当前进程。

|#表示导致当前调试事件的进程。|*表示当前系统的所有进程。

|n为序号为n的进程。

|~pid表示进程IDpid的进程。

~为线程限定符

~.表示当前线程。

~#表示导致当前调试事件的线程。

~*表示当前系统的所有线程。

~n表示序号为n的线程。

~~tid表示线程IDTID的线程。

可以使用~0k来显示0号线程的栈回溯。

理解上下文

根据windows操作系统的特征,WinDbg定义了几种上下文:会话上下文、进程上下文、寄存器上下文和局部上下文。

会话上下文

Windows支持同时有多个会话。所谓会话上下文就是当前系统或者陈述所基于的登陆会话语境。对于会话A的所有进程来说,会话A的状态和属性便是它们的会话上下文。使用!session命令可以显示或切换会话上下文。每个进程的EPROCESS结构的session字段记录着这个进程所属的会话。使用!sprocess扩展命令可以列出指定的会话中的所有进程。

注意只有在内核调试时才有意义。!session!sprocess也只能在内核调试中使用。

进程上下文

所谓进程上下文就是指当前操作或者陈述所基于的进程语境。在内核调试时,如果要观察内核空间的数据,可以不必关心当前进程是哪个。因为内核空间是所有进程共享的。但是如果要观察用户空间数据就要指定进程。winDbg.process命令可以观察和设置默认进程。如:

其中85fd8298EPROCESS结构的地址。

使用!process00可以列出系统中所有进程的基本信息。注意只能在内核调试时使用。

另一个相关的命令是.context命令。它可以显示或设置用来翻译用户态地址的页目录基地址。对于x86系统cr3寄存器用来存放页目录基地址,每个进程的用户空间都是基于一个页目录基地址的。因此.context.process命令效果几乎是一样的。当调试用户态目标时,所有虚拟地址都是基于当前进程的,不需要切换进程上下文。因此.process.context都只能用在内核调试中。当一个调试会话调试多个用户态目标时,可以使用|进程号s来切换当前进程。

寄存器上下文

所谓寄存器上下文就是寄存器取值所基于的语境。Cpu寄存器保存的是当前正在执行线程的寄存器值。对于没有执行的线程它的寄存器值保存在内存中的CONTEXT结构中。当我们在调试器中观察一个线程的寄存器时,这个线程是处于挂起状态的。所以我们看到的值都是保存在CONTEXT结构的值,而不是此时物理寄存器的值。

系统会在以下情况将cpu寄存器的值保存在当前线程的CONTEXT结构中:

1:当做线程切换时,系统会将要挂起线程的寄存器值保存在CONTEXT结构中。这个上下文常被称为线程上下文。

2:当发生中断或异常时,系统会将当时的寄存器值保存起来。这个上下文被称为异常上下文。

使用.thread命令可以显示或者设置寄存器上下文所针对的线程。

使用!process<进程EPROCCESS地址>f可以列出一个进程的所有线程。将ETHREAD结构地址作为.thread命令的参数可以将这个线程的上下文设置为当前线程上下文。

局部上下文

所谓局部上下文就是指局部变量所基于的语境。因为当前函数和局部变量都是与栈帧密切相关的,所有WInDbg使用栈帧号来代表局部上下文。使用.frame可以观察当前局部上下文。

使用.frame加栈帧号可以切换到指定的栈帧。

此时可以使用dv命令来显示该栈帧下的参数和局部变量。注意该命令需要私有符号的支持。

调试符号

符号文件(SymbolFiles)是一个数据信息文件,它包含了应用程序二进制文件(比如:EXEDLL等)调试信息,专门用来作调试之用。最终生成的可执行文件在运行时并不需要这个符号文件,但你的程序中所有的变量信息都记录在这个文件中。所以调试应用程序时,这个文件是非常重要的。用VisualC++WinDbg调试程序时都要用到这个文件。

Windows系统中,符号文件以.pdb为扩展名。比如:每个Windows操作系统下有一个GDI32.dll文件,编译器在编译该DLL的时候会产生一个GDI32.pdb文件,一旦拥有了这个PDB文件,那么便可以用它来调试并跟踪到GDI32.dll内部。该文件和二进制文件的编译版本密切相关。比如:如果修改了DLL的输出函数,再编译该DLL,那么原先的PDB文件就过时了,不能再用老的PDB文件来做调试工作,而必须使用最新的PDB文件版本。

VisualC++编译代码后会在Debug或者Release目录下生成一个PDB文件。一般情况下,符号文件包括以下的数据信息:

1.全局变量(Globalvariables);

2.局部变量(Localvariables);

3.函数名和它们的入口地址(Functionnamesandtheaddressesoftheirentrypoints);

4.FPO数据(FramePointerOmission)FramePointer是一种用来在调用堆栈(Callstack)中找到下一个将要被调用的函数的数据结构源代码的行序号(Source-linenumbers);

大多数调试任务都涉及到多个模块,因此需要加载多个符号文件。为了方便调试windbg允许用户指定一个目录列表,当需要加载符号文件时,WinDbg会自动从这些目录搜索合适的文件。这个目录列表被称为符号搜索路径。在符号路径中可以指定两类位置:一类是普通的磁盘目录或网络共享目录的完整路径。另一类是符号服务器。多个位置使用分号分隔。

可以有多中方法设置符号路径:

一:设置环境变量_NT_SYMBOL_PATH_NT_ALT_SYMBOL_PATH

二:启动调试器后,在命令行中通过-y开关来定义。

三:使用.sympath命令来增加、修改或显示符号路径。如.sympath+c:\symbols来将c:\symbols目录加入到搜索路径中。

四:使用.symfix命令来自动设置符号服务器。

五:使用GUI图形界面设置符号搜索路径。

输入.sympath不带任何参数可以显示当前符号路径。

Windbg使用所谓的懒惰式符号加载策略。当它受到模块加载事件时,并不会立即为这个模块加载符号文件。我们在观察模块列表时会看到很多模块的符号状态都是deffered,即推迟加载。

观察模块信息

lm命令可以用来观察模块信息。如果不指定参数,该命令会显示已加载模块。如果想显示更详细的信息可以使用v选项。

如果想控制要显示的模块,可以使用以下方法:

一:使用m开关可以指定对模块名的过滤模式。比如lmmk*仅显示以k开头的模块。

二:使用M开关可以指定对模块路径的过滤。

三:使用o开关可以只显示已加载的模块,排除已卸载的模块。

四:使用l开关只显示已经加载符号的模块。

五:使用e开关可以只显示有问题的模块。

检查符号

使用x命令可以用来检查调试符号。其格式为:

x[选项]模块名!符号名

模块名和符号名都可以包含通配符,*代表任意个字符,?代表一个字符,#代表该符号前面的字符出现任意次。

如:xnotepad!g*

因为公开的符号信息不包括类型信息,所以上述符号类型部分都显示为notypeinformation

搜索符号

ln命令用来搜索距离指定位置最近的符号。如

上面的结果显示地址00fd3689附近有两个符号。其中notepad!WinMainCRTStartup与指定地址精确匹配。

事件处理

Windows的调试模型是事件驱动的。调试目标是调试事件的发生源,调试器负责接收和处理调试事件,调试子系统负责将调试事件发送给调试器并为调试提供服务。简单来说,异常是调试事件的一种。因为有很多中异常,所以异常事件又分为多个子类。而其他调试事件却没有包含子类。

对于每个异常windows系统最多给予两轮处理机会,对于每一轮记录调试子系统都会试图先发给调试器,然后再寻找异常处理器。因此对于每个异常,调试器最多收到两次处理机会。每次处理后都应该向系统返回一个结果,说明它是否处理这个异常。

对于第一轮异常处理机会,调试器通常是返回没有处理异常。然后让调试子系统继续分发,交给程序中的异常处理器来处理。对于第二轮机会如果调试器不处理,那么系统便会终止应用程序执行或启用蓝屏机制停止整个系统。所以对于第二轮异常处理机会,调试器通常返回已经处理,让系统恢复程序执行。这通常会再次导致异常,又重新分发异常,如此循环。值得注意的是,对于断点和调试异常,调试器会在第一轮处理机会就返回已经处理。

Windbg将异常和其他调试事件一起处理,但只有异常事件有两轮处理机会,异常以外的事件只有一次处理机会。

大多数调试器都允许用户来定制处理调试事件的方式。因为异常事件最多有两轮处理机会。而且对于每一轮机会都需要决定如下几个问题:

一:当收到事件通知时是否中断给用户。

二:返回给系统的处理结果。是返回已处理还是没有处理,即所谓的处理状态。

三:第一轮机会是否中断给用户。

四:第二轮机会是否中断给用户。

五:第一轮机会的处理结果。

六:第二轮机会的处理机会。

前两个选项被称为中断选项。后两个被称为继续选项。

为了允许用户设置这些选项,不同调试器提供了不同形式的界面。下图为WinDbg调试器的事件配置

右下角的Execution组的四个单选按钮用来配置中断选项,它们的含义如下:

Enable:收到该事件后便中断给用户。对于异常事件,意味着两轮事件都中断给用户。对于其他事件,意味着收到便中断。

Disable:对于异常事件,第二轮机会时中断给用户。第一轮不中断。对于其他事件不中断。

Output:输出消息通知用户。

Ignore:忽略这个事件。

Continue组的两个单选按钮,用于配置返回给系统的异常事件处理状态,只适用于异常,且针对第一轮处理机会。如果选择Handled,那么便返回已经处理异常,否则返回没有处理异常。对于大多数异常,默认返回时没有处理异常。对于第二轮默认返回已经处理。WinDbg即允许配置中断选项也允许配置继续选项。但是vc只允许配置中断选项,不允许设置继续选项。

下图为vc2010调试器配置界面:

上图中,对于每种异常,提供了两个复选框。分别为thrown(引发)和User-unhandled(用户未处理)。前者的含义是对于第一轮机会是否中断给用户。后者的含义是对于第二轮异常是否中断给用户。从上图也可以看出,我们无法配置继续选项。

控制调试目标

当调试一个新创建的进程时,为了让调试人员可以尽早的分析调试目标,windows操作系统的进程加载器提供了特别的调试支持。在完成最基本的用户态初始化后,系统的模块加载函数就会主动执行断点指令,触发断点,使调试目标中断到调试器。这个断点被称为初始断点。

初始断点是位于ntdll中的LdrpInitializeProcess函数调用DbgBreakPoint而触发的断点。在创建一个新进程时,很多的初始化工作都是在父进程的环境下完成的。初始线程正在在新进程空间执行是从内核态的KiThreadStartup开始的。LdrpInitialize函数是一个新进程的初始线程开始在用户态执行的最早代码。然后LdrpInitialize会调用LdrpInitializeProcess函数。LdrpInitializeProcess函数会检查当前进程是否是在被调试状态。如果是则调用DbgBreakPoint通知调试器。 恢复执行后,系统开始执行已经放在线程上下文中的进程启动函数BaseProcessStart。该函数会调用程序的入口函数使应用程序开始运行。

当将WinDbg附加到一个以运行的进程时,WinDbg默认会通过在目标进程创建远程线程来触发一个初始断点。

WinDbg的命令行中加入-g开关,可以让其忽略或者不乏其初始断点。

另外需要说明的是初始断点并不是调试器可以得到的最早控制机会。进程创建事件和exe模块的加载事件都比初始断点要早。

俘获调试目标

初始断点为我们在进入时分析被调试程序提供了一个初始机会。如果希望在以后把运行的调试目标再次中断到调试器中,可以使用以下方法:

一:在调试界面中选择中断命令(Debug-Break)或者使用Ctrl+Break热键。

二:在被调试进程按F12.

三:设置断点,让程序触发。

第一种方法使用比较广泛。它是在调试目标进程中创建远程线程来实现的。

远程线程的函数体比较简单,仅仅是调用DbgBreakPoint。在恢复执行后,该远程线程便结束执行。

但是并不是在任何时候通过创建远程线程都可以使被调试进程中断到调试器。比如当远程线程还未触发断点指令时就被挂起时,这时WinDbg就收不到断点事件了。对于这种情况,WinDbg会等待30s。然后调用函数挂起被调试进程的所有线程,从而强制将被调试进程俘获到调试器中。

恢复运行

WinDbg提供很多命令来让调试目标恢复运行。最常用的是g命令。热键F5Debug菜单的Go菜单项对应的就是g命令。其格式为:

g[a][=startAddress][BreakAddress...[;BreakCommands]]

startAddress用来指定恢复执行的起始地址。默认为当前位置。

BreakCommands用来指定断点命中后所执行的命令。只有在指定BreakAddress时开关a才有用。使用a将断点设置为硬件断点。如果没有a则设置软件断点。

如果在g命令中指定了断点地址,那么WinDbg会设置一个隐藏的断点,然后恢复目标执行。当执行到这个断点时,WinDbg会中断并自动删除这个断点。调试器的‘运行到光标处’就是使用g命令加断点地址来实现的。

为了提高灵活性,可以使用gngh命令来指定要恢复给系统的异常处理决定。gh告诉系统该异常已经处理。gn告诉系统该异常未处理。gu命令用来执行到上一级函数,即执行完当前函数。该命令没有参数。

单步执行

单步执行分为源代码级的单步和汇编指令一级的单步。如果当前的指令或代码行包含函数调用,有两种选择:一种是跟踪进入,另一种是跳过。前一种方式称为单步进入,对应的WinDbg命令为t,后一种方式称为单步跳过,对应的命令为p

p命令和t命令的格式为:

p|t[r][=startAddress][count][command]

r选项用处是禁止自动显示寄存器内容。默认情况下每次单步执行后,会自动显示各个寄存器值。如果不想显示,则使用r开关。

默认情况下,调试器总是让目标程序从当前位置开始单步执行。但是可以通过等于=符号,来指定一个新的起始地址。但要注意该功能可能会导致栈失衡。

可选的参数[count]用来指定要单步执行的次数。如果count大于1,那么执行好一次后,WinDbg会再发送一个单步命令,知道到达指定的次数。[command]参数用来执行每次单步执行后要执行的命令。比如p2kb可以在每次单步执行后执行kb命令。

单步执行到指定地址

pata命令用来执行到指定的代码地址。其格式为:

pt|ta[r][=startAddress]StopAddress

paSteptoAddress的缩写。在执行过程中,WinDbg会显示程序执行的每一步,其效果相当于反复执行p命令。tapa很相似,只是遇到函数调用会跟进到函数中。因为伪寄存器$ra代表当前函数的返回地址。因此可以使用pata命令加上@$ra来跳出当前函数。其效果相当于gu命令。

单步执行到下一个函数

pctc命令用来单步执行到下一个函数调用指令(CALL),其格式为:

pc|tc[r][=startAddress][Count]

如果不指定startAddress,该命令就会从当前位置开始执行。指定时则从指定位置开始执行。直到遇到函数调用指令时停下来。Count参数用来指定遇到的函数调用指令个数。默认为1

pctc的区别与patc的区别类似。

追踪并监视

如果我们想了解一个函数的执行路径和它调用了那些其他函数,以及每个函数包含了多少条指令,但我们又不想一步步的跟踪执行。此时我们就可以使用wt命令。其格式为:

wt[WatchOptions][=StartAddress][EndAddress]

可选开关WatchOptions 用以控制显示格式。

-ldepth指定调用显示的最大深度。超过深度的指令不再显示。

-mmodule限制只显示某一模块的代码。

-imodule忽略指定模块的代码。

StartAddress指定开始执行的指令地址。如果不指定StartAddress,则会从当前指令地指处开始执行。如果该地址是函数入口地址,则执行完整个函数。如果不是函数入口则wt效果相当于p命令,仅仅执行一条指令。

EndAddress指定wt命令结束的指令地址。如果不指定EndAddress,则会执行一条指令或一个函数(根据startAddress是否是函数入口地址)。

如下图的报告:

该报告包括六个部分。

第一个部分是标题。显示了所追踪的函数名和追踪的结束地址,即函数返回地址。

第二部分是详细的执行情况表。分成若干行,每一行用来描述一段执行路线,每次函数变换会重新开始一行。第一列为本行所描述的函数已经执行的指令数。第二列用来显示本行所描述的函数所调用的子函数内所执行的指令数。第三列用来表示函数调用的深度,被追踪的函数深度为0。每进入一个函数,深度加1。每返回一次深度减1。第四列为函数名称,名称前的缩进长度与调用深度是成比例的。

第三部分是对被追踪函数的简单归纳。包括指令的指令数和处理的调试事件数。

第四部分是按函数统计的指令表格。每一行是执行过的一个函数。表格的前两列分别是函数名称和调用次数。后三列是这个函数每次执行时的最少指令数、最多指令数和平均指令数。

第五部分是调用系统服务的情况。

第六部分是追踪执行完成后的寄存器状态和当前程序指针的位置。显示的是函数返回到上一级函数后即将执行的下一条指令。

如果使用wt命令追踪复杂的函数,那么可能需要较长时间。为了提高效率,可以通过命令选项来限制追踪的范围。如-l选项指定追踪的深度,超过这一深度的函数调用可以一次执行。-m开关来指定追踪的模块。-i开关可以指定忽略的模块。如果在追踪过程中遇到断点或者发生其他调试事件,那么wt会被中断而停止。

断点

断点分为软件断点和硬件断点。

软件断点

软件断点是通过将指定位置的指令替换为INT3指令而设置的断点。有三条命令来设置软件断点。分别是bpbubmbp是最常用的。

其格式如下:

bp[ID][Option][Address[Passes]][CommandString]

ID为断点编号,如果不指定,Windbg会自动为其选择一个编号。 Options用来指定选项。

Address用来指定断点地址。如果不指定则默认使用当前EIP所代表的地址。

Passes用来指定穿越次数。每次命中时其值减1,并回复程序执行,减为0时出发断点。默认值为1

CommandString用来指定一组命令,当断点中断时,WinDbg会自动执行这组命令。

bptest!Func+3kv;dapoi(ebp+8)

上面的命令会在Func函数入口偏移3的地址处设置断点。当断点被触发时,中断给用户并自动执行kvdapoi(ebp+8)命令。

ebp+8只有在建立栈帧后才能指向第一个参数。因此需要在Func入口偏移3的地址处设置断点。而不是Func处。

bm用来设置一批断点。相当于执行多次bp命令。如:bmtest!fun*。该命令会在所有以fun开头的符号处设置断点。

bu命令用于设置一个延迟的以后再落实的断点。

硬件断点

硬件断点在x86系统下是通过cpu的调试寄存器来设置的断点。硬件断点具有数量限制,可以实现软件断点不具有的功能。WinDbgba命令用来设置硬件断点,其格式如下:

ba[ID]AccessSize[Option][Address[Passes]][CommandString]

ba指令与bp指令很类似。

Access用来指定触发断点的访问方式,可以为以下几个字母之一:

e:用于设置代码访问断点。这种断点被称为代码硬件断点,与软件断点类似,但是硬件断点的好处是不需要做指令替换和恢复。

r:当从指定地址读取和写入数据时触发断点。

w:当向指定地址写入数据时,触发断点。rw选项设置的断点又称为访问数据断点。

i:当向指定地址执行输入输出访问是触发断点。这种断点又被称为访问IO断点。

size用于指定访问的长度。对于访问代码硬件代码,size的值应该为1。对于其他硬件断点可以有124三种值。分别代表1字节访问、字访问和双字访问。

因为cpu是根据实际访问是否包含断点定义区域来判断是否命中,所有当实际访问长度大于断点定义的访问长度时,断点也会命中。

Address参数用来指定断点地址。主要地址值是按照size的值做内存对齐的。

其他参数与软件断点一样。不再重复介绍。

硬件断点是设置在cpu的调试寄存器的。在x86系统cpu中就是DR0~DR7

最后需要注意的一点是当初始断点命中时还不能设置硬件断点。因为初始断点之后,系统还会设置线程上下文。建议在执行到程序的入口地址后再设置。

条件断点

前面介绍的软件断点和硬件断点都支持一组关联命令。当断点命中时WinDbg会自动执行这组命令。我们可以在这组命令中判断是否需要中断到调试器。如果不需要则立即恢复执行。为此可以在关联命令中使用j命令或.if。。.else命令。即:

bp|bu|bm|baAddressj(Condition)‘’;gc

bp|bu|bm|baAddress.if(Condition){OptionalCommands}.else{gc}

Condition用来定义希望中断的情况。

OptionalCommands用来定义条件满足时执行的命令。

gc是当条件不满足时执行的命令。它的作用是从导致中断的断点处恢复执行。

地址表达方法。

可以使用以下三种方法来指定断点命令中的地址参数:

一:使用模块名加函数符号的方式。比如:bptest!WinMain。也可以在符号后加一个偏移。

二:直接使用内存地址。比如bp00411390

三:如果是使用完全的调试符号,该完全的调试符号中包含源代码行信息,那么可以使用如下形式:

[[module!]FileName][:LineNumber]

其中module是模块名,FileName是源程序文件名,LineNumber为行号。整个表达式使用两个中音符号(‘’)包围。

如:bptest!main.cpp:16

该命令是对main.cpp源代码的16行设置断点。其中test!开始省略。

四:对于C++类方法,也可以使用类名双冒号::或双下划线来连接类名和方法名。如:bpclassA::func,bpclassA__funcbp@@(classA::func)

注意:在使用前两种方法设置软件断点时,应该确保断点地址指向的是指令的起始处,而不能是指令的中部。因为调试器会将此地址处设置为INT3指令。当cpu指定到此处时,会认为这是一条多字节指令,会将设置断点处得指令与后面的指令结合在一起执行。这会导致无法预料的错误。

设置针对线程的断点

在设置断点时,可以在命令前添加线程限定符~,指明要设置断点的线程。如~0bpMSVCR80D!printf。这个断点是与线程相关的。只有当0号线程执行到这个函数时才会中断给用户。

注意该方法只能针对用户态调试的情况。

管理断点

使用bl命令可以列出当前已经设置的所有断点。

第一列是断点序号。

第二列是断点状态:e代表启用,d代表暂时禁止使用。

第三列是断点地址。可以使用执行源文件加行号方式设定。第四列与第五列与穿越次数有关。

第四列表示还需要穿越多少次才能中断到调试器。

第五列为穿越初始值。默认为1

第六列是断点所关联的进程和线程。冒号前进程。冒号后是线程号。****代表这个断点是针对所有线程。

第七列是断点地址的符号表示。

如果断点有关联的命令,该关联命令会显示在第七列之后。

命令bcbdbe分别用来删除、禁止、启动断点。它们的格式都是:bc|bd|be断点号。

断点号为*时,代表所有断点。使用-来表示一个范围,如:bd0-4。也可以使用逗号来指定多个断点:bd1,2

控制进程和线程

当被调试程序中断到调试器时,其所有线程都被挂起。当恢复执行时,所有线程都被恢复执行。但是调试器可以根据需要而保持某些线程仍处于运行状态。

第一种方法是通过增加线程的挂起计数来禁止线程恢复执行。可以使用~ThreadNumn来增加线程号ThreadNum的挂起计数。如:~1n。增加1号线程的挂起计数。与~n相对,~m命令用来减少线程的挂起计数。

第二种方法是使用~f~u命令来冻结(Freeze)和解冻一个线程。当一个线程处于冻结状态时,在恢复目标执行时,冻结的线程不会恢复执行。

从实现角度看,~m~n是调用操作系统的API来改变线程的挂起计数。而~f~u命令完全是调试器内部维护的一个线程属性。在恢复执行时,调试器会对所有线程调用ResumeThread。而一旦这个线程被设置为冻结状态,WinDbg便不再对其调用ResumtThread

第三种方法是在恢复执行的命令前通过线程限定符和线程号码来恢复执行指定的线程。如:~0g只恢复0号线程执行。使用此方法恢复一个线程执行时,通过Ctrl+Break创建远程线程来让调试目标中断到调试器的方法就不能使用了。因为该远程线程一创建便被挂起了。

多进程调试

WinDbg支持使用一个调试器来调试多个进程。在调试一个进程时,还可以调用.attach命令把另一个进程加入调试会话中。如:.attach0n2488。注意此处2488前是0n代表十进制的进程号。

上面的提示信息告诉我们,调试器已做了必要的登记。将在下次恢复目标执行时执行附加的动作。

恢复调试目标执行后,新附加的进程会附加到调试器中。命令提示符如下:

1代表进程号。2代表1号进程的2号线程。注意:线程号是全局编排的,1号和0号进程的线程共享一套线程号。

可以使用|观察所有进程。

使用|<进程号>s可以切换当前进程。

观察栈

栈记录了软件运行的丰富信息,观察和分析栈是软件调试的一种重要手段。

栈回溯:从栈顶向下遍历每个栈帧来追溯函数调用过程。

WinDbgk系列命令,可以进行栈回溯。

k命令执行结果如下:

上图中的每一行描述当前线程的用户态栈的一个栈帧。最上面的一行描述的是程序指针所对应的函数。也就是当本线程中断到调试器时正在执行的函数。每个函数下面的一行是上面的一行的上一级函数,称为父函数。

横向来看,第一列是栈帧的基地址。因为x86使用EBP来记录栈帧基地址,所以这一行的标题叫做ChildEBP

第二列是函数返回地址。这个地址是返回到父函数中的指令地址。通常就是调用本行函数的CALL指令的下一条指令的地址。

第三列是函数名和执行位置。对于正在执行的函数(第一行)执行位置表示的是当前EIP指向的位置。对于父函数来说(从第二行开始)执行位置代表子函数返回到父函数时将执行的指令地址。这个值其实是通过寻找距离返回地址最近的符号而得到的。与第二列表示的返回值意义相同,只是表示方式不同罢了。通过第三列符号加偏移的表示方法,我们可以得出函数调用关系。

k命令仅仅显示函数名信息,但是不显示参数。kb命令除了显示函数名信息以外还显示该函数在栈上的前三个参数。

如:

如果符号文件包含私有符号,使用kp命令可以根据符号文件中的函数原型信息,将参数和参数值都以函数原型的格式显示出来。

kv命令可以在kb命令的基础上增加显示FPO信息和调用协议。

kn命令会在每行前显示栈帧的序号。

另外还可以为所有的命令都指定f选项。这个选项会显示每两个相邻栈帧的内存距离。即栈帧基地址的差值。这可以帮助我们观察栈的使用情况。

观察栈变量

使用dd命令加上栈帧地址可以观察站栈帧的原始内存。使用dv命令可以帮助我们以更友好的方式显示栈上的局部变量。

注意该命令需要私有符号的支持。对于没有私有符号的模块。dv命令是无法工作的。如果要观察父函数的局部变量,可以使用.frame命令加上父函数的栈帧号来切换到那个栈帧,再使用dv命令。

分析内存

内存是软件工作的舞台。内存空间是通过地址来标识和引用的,内存地址有多种。常用的有物理内存地址和虚拟内存地址。下面介绍的地址除非特别说明外都是指虚拟内存地址。

显示内存区域

d系列命令可以用来显示指定地址的内存区域。其格式为:

d{a|b|c|d|D|f|p|q|u|w|W}[Option][Range]

dy{b|d}[Options][Range]

d[Options][Range]

其中大括号中的字母用来指定数据显示的方式。是区分大小写的。a表示ASCII码,d表示DWORD,D表四doublec表示DWORDASCII码。f表示floatp表示按指针宽度显示。q表示quadu表示UNICODEw表示字。W表示字和ASCII码。yb表示二进制和字节。yd表示二进制和双字。

Range表示显示的范围。可以有以下几种方式:

一:起始地址加空格加终止地址。如dd0012fd9c0012fda8

二:起始地址加空格加L和元素个数。如dd0012facL4

三:终止地址加空格加L加负号和对象个数。如dd0012facL-4

如果没有指定显示格式,而直接执行d命令,那么将采用最近使用过的数据显示格式。

显示字符串

对于/0结尾的字符串可以使用dadu命令来显示它的内容。当遇到/0时,会自动停止显示。

如果使用da命令来显示UNICODE字符串,由于UNICODE第二个字符为0,因此只能显示一个字符。

显示数据类型

dt命令用来显示数据类型和按照类型来显示内存中的数据。

可以使用dt[模块名!]数据类型来显示一个数据结构。如

dt_EPROCESS

其中模块名可以省略。可以使用-r开关来指定显示的深度。-r0表示不显示子类型。-r1代表显示一级子类型。

如果不想显示整个结构,而只显示某些字段,可以在类型名后使用-ny开关附加搜索选项。

dt_TEB-nyLastError

该命令仅仅显示TEB结构的LastError字段。

还可以在上述命令后加内存地址。让dt按照类型显示指定地址的变量。例如:dt_PEB7ffdd000命令可以把内存地址7ffdd000处得数据按_PEB的结构显示出来。

第三种用法是显示类型。包括全局变量、静态变量和函数等。dt命令会显示该符号的类型。如果是函数,dt会显示它的参数和取值。

搜索内存

可以使用s命令来搜索内存。有三种方法:

第一种方法是在指定的内存范围内搜索任何ASCIIUNICODE字符串。格式为:

s-[[Flags]]sa|suRange

Range指定内存范围。其写法与d命令的Range一样。

sa开关用于搜索ASCII字符串。su用来搜索UNICODE字符串。

[Flags]用来指定搜索选项。可以用l加一个整数来指定字符串的最小长度。使用s将搜索结果保存起来,然后r再在保存的结果中搜索。

如:s-[l5]sa023332321200。上面的命令表示从内存地址02333232处开始到1200字节范围内搜索长度不小于5ascii字符串。

第二种方法是在指定的内存地址范围内搜索与指定对象相同类型的对象。格式为:s-[[Flags]]vRangeObject

如:s-v0x12fc30110000x12fe4c

第三种方法是在指定范围内搜索某一内容。其格式为:

s[-[Flags]Type]Rangepattern

Type用来指定要搜索内容的数据类型。可以为b(字节)w()d(双字)q(四字)、a(ASCII)或者uUNICODE)。如果不指定类型,默认类型为b。即按字节搜索指定的内容。

Range用来指定搜索范围。

pattern用来指定要搜索的内容。可以用空格分隔要搜索的数值。

如:s-w0x40000012a000416476446267

如果是按字符串搜索,需要用双引号包围要搜索的内容。如:

s-w0x40000012a000AdvDbg

修改内存

命令e用来修改指定内存地址或者区域的内容。

第一种是按字符串方式编辑指定地址的内容,其格式为:

e{a|za|zu}AddressString

Address为要修改内存的起始地址。

za代表以0结尾的ASCII字符串。zu代表以0结尾的UNICODE字符串。au分别代表不是以0结尾的ASCIIUNICODE字符串。

使用物理内存地址

上面介绍的都是虚拟地址,如果要显示和修改物理地址,需要使用扩展命令!d{b|c|d|p|q|u|w}。其用法与前面介绍的类似。但注意只有在内和调试时才能使用此命令。

观察内存属性

使用扩展命令!address可以显示某一个内存地址或区域的特征信息,其格式为:

!address[Address]

Address为要观察内存地址。如:

第三行的含义是指定的地址是属于一个从00e11000开始的一个较大的区域中以00e11000开始的较小的内存区,这个内存区大小为00053000

第四行为内存类型,可以包含以下标志位:MEM_IMAGEMEM_MAPPED或者是MEM_PRIVATE分别代表从执行映像文件映射的内存、从其他文件映射的内存和私有内存。

第五行是内存的页属性,可以包含以下标志位:PAGE_READONLYPAGE_READWRITEPAGE_NOACCESSPAGE_EXECUTEPAGE_GUARD等值。

第六行是内存状态,可以为MEM_COMMITMEM_RESERVEMEM_FREE。分别代表已提交的内存、保留的内存和标志为释放的内存。

第七行为内存区的用途,可以为以下常量:RegionUsageIsVAD(虚拟地址描述符VAD)RegionUsageFree(空闲)RegionUsageImage(执行映像的映射)RegionUsageStack()RegionUsageTeb(线程环境块)、RegionUsageHeap()RegionUsagePageHeap(页堆)、RegionUsagePeb(进程环境块)RegionUsageProcessParameter(进程参数)、RegionUsageEnvironmentBlock(环境块)

第八行会因内存用途不同而不同。

如果不指定参数!address命令会显示当前进程中所有内存区域和关于这些区域的统计信息。

在内核调试中还可以使用!pte命令来显示指定虚拟地址所属的页表表项PTE和页目录表项PDE

第三行显示的是PDEPTE的虚拟地址。

第四行是PDEPTE的内容。

第五行是将第四行的内容按高20位和低12位分解为PFNPageFrameNumber)和页属性。PFN用来转换物理地址,其规则是将其乘以0x1000(页大小)。然后再加上虚拟地址的页内偏移。因此,以上虚拟地址的物理地址为:154*0x1000+4F4=1544F4

可以分别使用!dddd命令观察内存1544F4801544F4来验证是否一致。

页属性中的G代表全局,D代表数据,A代表访问过,K代表这是内核态拥有的内存页。W代表可以写,E代表可以执行,V代表有效。

以上参考自《软件调试》张银奎著,如有纰漏,请不吝指正!

2013、3、3于山西大同

今天就要去动身去杭州了,即将开始新的工作、学习和生活。希望在新的一年有更大的进步!

<!--EndFragment-->
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics