Fork me on GitHub

使用WinDbg进行内核调试(2010-11-19 17:17:05)

Windows调试工具中的调试器(debugger)功能强大,但需要艰苦的学习,尤其是WinDbg和KD这些驱动开发程序员使用的内核调试器(CDB和NTSD是用户态调试器)。针对的目标是使用过其他调试器的有经验的程序员,以便开始进行内核调试或用来获取Windows调试工具帮助文件的参考内容。这些程序员我们假定已经熟悉Windows操作系统的一般概念及构建过程。

大多数情况下我们重点关注WinDbg这个内核模式和用户模式调试器,它有一个图形接口。KD在用于脚本调试和自动调试时更有用,而且多数严谨的程序员更喜欢使用它,但是本文档将重点关注Windbg,只是偶尔提及KD。

目标操作系统是Windows2000或更新版本。这里的讨论多数也适用于Windows NT,而且,目标计算机使用X86架构,尽管多数在64位目标机也能工作,但不特别提及。

概述中,本指南对调试器的安装进行简单的描述。指南的主题包含两部分的章节:原理和技巧。原理是经常被使用的基本调试器命令;技巧则是那些在很多情况下很有用的其他命令。稍后的章节对诸如死锁、无效内存和资源泄漏这类的技巧的讨论,首次阅读本指南可能你想跳过有关技巧部分的内容。本指南结尾有微软调试器讨论组和信息反馈信息的电子邮件地址用于解决以后遇到的问题。

准备工作

得到最新版本

获取最新的调试器并定期更新它,要保持使用最新的版本,因为调试器经常被完善或改进,调试器可以在下面的地址下载:

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

连接主机和目标机

调试是在两台计算机之间进行的,它们通过直连串口缆线或1394缆线被连接起来。本指南不讨论在单机上的本地调试。三机调试(目标机、调试服务器和调试客户机)将被少量提及。

一个调试会话是主机端调试程序(WinDbg或KD)进程和目标操作系统的联合体。特别要指出的是,windbg不是运行在目标机上的Hypervisor操作系统,而是工作在一个实际存在的操作系统之上。Windbg与目标操作系统是伙伴关系,后者了解其在调试进程中的角色。在这个伙伴关系中,目标机从发送信息并从windbg接收信息。通讯机制必须是好的、高效的。

串口协议对调试器和目标机之间通讯来说是可靠的机制,我们在二者之间通过一个直连串口缆线连接起来,另一个可选的机制是1394。Windows调试工具的帮助文档的“(在目标机上配置软件)Configuring Software on the Target Computer”描述了这两种方式如何配置目标操作系统。

你的第一个会话

你的主机被假定工作在Windows2000或更新的操作系统上,主机操作系统可以是与目标机不同的版本。主机可以是你常用的开发、维护或解决问题的计算机,它应该被连接到网络上,如果你想访问符合和源代码服务器的话(参见符号和源代码symbols and source)。

在命令窗中改变当前目录到Windows调试工具安装所在的地方,键入windbg并回车你将看到这个窗口

 

窗口

你可以安排你的窗口,下面的例子使用了浮动窗。开始是上面出现的联合窗,点击标记为“Command”的窗口的标题栏,把它拖拉到主窗口外面。

然后选择“FileàKernel Debug”得到协议弹出框,选择1394,使用1号通道,这时你的桌面就像这样:

然后点击内核调试窗的OK按钮。

使连接可用

现在准备连接主机和目标机,从调试入口启动目标机,然后立即用鼠标点击主机命令窗,并按下CTRL+BREAK,几秒钟之后你应该能看到这个:

现在准备连接主机和目标机,从调试入口启动目标机,然后立即用鼠标点击主机命令窗,并按下CTRL+BREAK,几秒钟之后你应该能看到这个:

 

不要担心关于连接符号的信息,你已经连接到了Windows server 2003

一件小事但很关键:命令窗底部区域显示的“kd>”提示符,这意味着windbg准备在这里接受命令了。如果没有显示这个提示符,windbg这一刻就不能处理命令,即便你键入的任何命令被存储在缓冲区中且不久将得到执行。你必须等待“kd>”提示符的出现才能使windbg响应。Windbg有时处于忙状态,比如,从目标机获取信息并且信息量很大时,没有“kd>”是windbg忙的唯一线索。有可能是windbg正在工作以处理符号,而这个过程比你预期的时间要长,糟糕的是,偶尔也有可能windbg正在等待诸如与停止响应目标机建立连接(比如boot.ini配置错误,或选择了错误的选项)。你将不得不在经过一段时间之后采取极端的方式(比如,按下CTRL+BREAK)停止当前windbg,并再次启动一个新的windbg实例。

查找符号和源代码

现在你可能急于启动调试器,但是,你还应该再做点事情,因为这样才能极大地改善你的调试体验。

第一件事是确保windbg可以找到模块的符号。符号标识了二进制指令的代码语句以及哪些变量有效,换句话说,就是符号映射。如果你在构建模块的时候把它们生成了符号,你就可以使用这个模块和它的源文件。

但是如果你需要单步调试很久以前构建的代码怎么办?或者,你的代码不在当初构建的位置又怎么办呢?

为了显式设置符号的位置,使用.sympath命令,在命令窗中中断执行(CTRL-BREAK),然后输入:

.sympath SRV*<DownstreamStore>*http://msdl.microsoft.com/download/symbols

这样告诉windbg在微软公共符号服务器上查找符号,为了让windbg使用服务器并保持一个下载的符号库的副本到本地存储,例如D:\DebugSymbols,你可以这样:

.sympath SRV*d:\DebugSymbols*http://msdl.microsoft.com/download/symbols

偶尔,你可能在从符号库服务器上获取符号库时遇到困难,这种情况下,使用!sym noisy命令能获取关于windbg尝试获取符号的更多信息。然后,使用!lmi来查看必不可少的windows模块ntoskrnl的信息,接着使用.reload /f让 windbg尝试获取ntoskrnl的符号信息。就像这样:

kd> !sym noisy

noisy mode - symbol prompts on

 

kd> !lmi nt

Loaded Module Info: [nt]

         Module: ntoskrnl

   Base Address: 80a02000

     Image Name: ntoskrnl.exe

   Machine Type: 332 (I386)

     Time Stamp: 3e80048b Mon Mar 24 23:26:03 2003

           Size: 4d8000

       CheckSum: 3f6f03

Characteristics: 10e 

Debug Data Dirs: Type  Size     VA  Pointer

             CODEVIEW    25,  ee00,    e600 RSDS - GUID: (0xec9b7590, 0xd1bb, 0x47a6, 0xa6, 0xd5, 0x38, 0x35, 0x38, 0xc2, 0xb3, 0x1a)

               Age: 1, Pdb: ntoskrnl.pdb

     Image Type: MEMORY   - Image read successfully from loaded memory.

    Symbol Type: EXPORT   - PDB not found

    Load Report: export symbols

这里使用的命令的语法在windows调试工具的帮助文档中描述。

被导出的符号经常缺失,windows调试工具包含了一个符号库服务器,以便连接到微软的互联网站上的公共符号库,使用下面的命令添加符号路径并加载符号库:

kd> .sympath SRV*d:\DebugSymbols*http://msdl.microsoft.com/download/symbols

Symbol search path is: SRV*d:\ DebugSymbols *http://msdl.microsoft.com/download/symbols

kd> .reload /f nt

SYMSRV:  \\symbols\symbols\ntoskrnl.pdb\EC9B7590D1BB47A6A6D5383538C2B31A1\file.ptr

SYMSRV:  ntoskrnl.pdb from \\symbols\symbols: 9620480 bytes copied        

DBGHELP: nt - public symbols

         d:\DebugSymbols\ntoskrnl.pdb\EC9B7590D1BB47A6A6D5383538C2B31A1\ntoskrnl.pdb

kd> !lmi nt

Loaded Module Info: [nt]

         Module: ntoskrnl

   Base Address: 80a02000

     Image Name: ntoskrnl.exe

   Machine Type: 332 (I386)

     Time Stamp: 3e80048b Mon Mar 24 23:26:03 2003

           Size: 4d8000

       CheckSum: 3f6f03

Characteristics: 10e 

Debug Data Dirs: Type  Size     VA  Pointer

             CODEVIEW    25,  ee00,    e600 RSDS - GUID: (0xec9b7590, 0xd1bb, 0x47a6, 0xa6, 0xd5, 0x38, 0x35, 0x38, 0xc2, 0xb3, 0x1a)

               Age: 1, Pdb: ntoskrnl.pdb

     Image Type: MEMORY   - Image read successfully from loaded memory.

    Symbol Type: PDB      - Symbols loaded successfully from symbol server.

                 d:\DebugSymbols\ntoskrnl.pdb\EC9B7590D1BB47A6A6D5383538C2B31A1\ntoskrnl.pdb

       Compiler: C - front end [13.10 bld 2179] - back end [13.10 bld 2190]

    Load Report: public symbols

                 d:\DebugSymbols\ntoskrnl.pdb\EC9B7590D1BB47A6A6D5383538C2B31A1\ntoskrnl.pdb

符号库给出一些信息的时候,它们不提供源代码。最简单的情况下,源文件可以在它们被构建的地方找到(这里有二进制文件和符号文件)。但是多数情况下,会找不到它们(它们可能已经被移动过了)。因此,你必须指定到哪里去找它们,这就需要你知道源文件路径,例如:

.srcpath e:\Win2003SP1

这意味着:源文件在e:\Win2003SP1目录查找。

另一个方案是命名一个源文件服务器,比如:

.srcpath \\MySrcServer

如果你被获取源文件困扰,你应该使用.srcnoisy 1获取更多关于调试器查找源文件的信息。

工作区

你还没有开始实际的调试,已经键入了好的命令。许多设置可以被保存到一个工作区中,所以你应该使用FileàSave来保存设置到工作区,也许你会命名为kernel1394Win2003,之后你就可以通过工作区启动windbg:

windbg -W kernel1394Win2003 -k 1394:channel=1

–W指定了一个工作区,–k给出了通讯协议(参见windows调试工具帮助文档的“WinDbg Command-Line Options”)注意:windbg或KD中你应该小心保持命令行选项的大小写。

为了使得事情更容易,你可以放一个快捷键到你的桌面来通过想要使用的工作区启动windbg,例如,通过1394连接:

 

上面的文件的内容是:

cd /d "d:\Program Files\Debugging Tools for Windows"

start windbg.exe -y SRV*d:\DebugSymbols*http://msdl.microsoft.com/download/symbols -W kernel1394Win2003

第一行是将windows调试工具安装目录作为当前目录,以确保调试器能被找到。第二行启动windbg,并指定符号(-y)和工作区(-W)。

一个简单的驱动程序

通过一个简单的驱动程序的IoCtl来联系应该是有益的,例程在DDK以及其继承者WDK中,安装该工具包,在src\general\Ioctl 目录可以找到它。该例程的优势在于它是一个简单的传统驱动程序,也就是说,它是被服务管理器加载的,而不是一个即插即用的。你可以构建用户态可执行文件(ioctlapp.exe)和内核态驱动程序(sioctl.sys),因为前者将加载后者。

理解这些是简单但重要的。有关代码优化的构建过程是相当智能的,优化可以导致代码移动(当然要可维护)并在寄存器中保持变量值唯一。为确保简单易懂的调试体验,你应该在构建窗或应用程序中使用这样的编译指令构建一个chedcked版本:

MSC_OPTIMIZATION=/Od

(这里是“好吧,用d”,而不是“不要用d”,嘿嘿)

有时候使用像memcmp这样的内联函数时上面的编译指令会出问题。如果这样,试试下边的:

MSC_OPTIMIZATION=/Odi

要懂得防止对产品构建优化选项进行错误的选择,在上面的指令中,你将不能创建和测试一个产品类型的Build。作为一个好的习惯,总是用一个非优化Build进行测试,而且一旦你熟悉了代码并且排除了简单的错误,再进一步发展到产品Build。在你不得不处理优化代码的时候,你可以参见“处理优化代码”(dealing with optimized code)部分得到一些帮助。

开始调试这个简单的驱动程序

在IoCtl的DriverEntry函数设置一个断点,启动驱动之前,中断到windbg命令窗,键入:

bu sioctl!DriverEntry

bu (未解决的断点,Breakpoint Unresolved)命令在断点处延迟执行,直到模块被加载,到达这个点之后windbg将访问DriverEntry,因为没有什么要做的,可以按F5(你也可以键入g,即“Go”)。

 接下来,拷贝ioctlapp.exe和sioctl.sys到目标机的一个地方,比如C:\Temp\IOCTL,用管理员特权登录到系统,在一个命令窗里把C:\Temp\IOCTL设为当期目录(Windbg中不需要把这个路径放到符号路径或源代码路径中)。在命令窗中键入ioctlapp并回车,在windbg中你将看到:

在上面执行到断点时会停止,!lmi命令显示windbg从ddk build处捡取符号。时间戳正如你预料的那样,符号文件的位置也如预料的那样准确。

依赖于你停泊窗口的安排,有些窗口可能看不到,因为windows可以隐藏任何窗口,但某处有一个源代码窗口。键序列“alt-*”—不包含引号—可以使窗口到前面来。

标记为粉红色的那一行(windows调试工具称它为紫色)就是断点所在的地方,也是代码执行停止的地方。执行到IoCreateDevice的地方。

这里你再次看到了原先的断点(红色高亮,因为被控制停止在这里),而且你可以看到当前语句被标记为深蓝。

原理

那是调试会话的“试车”,这里是调试器的基本操作。

命令、扩展及其它

命令分为几个家族:朴素(不经修饰的)、句点打头的和惊叹号打头的。Windows调试工具帮助文档分别描述了这些家族的命令、元命令和扩展命令。对于当前的目的,这些家族都差不多。

断点

导致执行的中断,是调试器的一个基本能力,有几种方法可以做到。

·         在操作系统启动时中断

为了操作系统启动最早的点中断,确保windbg正确已连接,不停地按下CTRL-ALT-K,直到你看到:

 

在下一次引导的时候,操作系统将在ntoskrnl启动后很短时间内挂起,这时任何驱动都还没有被加载,这时windbg将获得控制权。当你希望定义断点在系统引导时启动驱动程序的时候,可以这样做。

·         普通断点

对简单的断点是通过bp(Breakpoint)命令设置的。例如:

bp MyDriver!xyz

bp f89adeaa

断点的第一个名字是模块名,第二个是给定的地址。当执行到这样一个点的时候,操作系统挂起并把控制权交给windbg(你可以在“查找一个名字 finding a name”看到怎么获取地址)。

注意:第一个命令的语法假定操作系统已经加载了模块并且通过符号文件可以得到足够的信息,如果xyz在模块中不能被找到,调试器就会通过外部名标识xyz找它。

·         延期断点

如果驱动程序没有被加载,你用bu设置的最早的第一个断点(参见上面的“开始调试简单的驱动程序” starting to debug the sample driver)就是一个延期断点。bu命令携带一个模块参数和一个名字,例如:

bu sioctl!SioctlDeviceControl

这里的SioctlDeviceControl是一个入口点或者sioctl.sys模块的其它名字。这种形式假定当模块被加载的时候,对标识符SioctlDeviceControl来说有足够的信息可用,以便断点可以被设置(如果模块已经被加载,并且名字也已经被找到,断点被立即设置)。如果操作系统无法找到SioctlDeviceControl,调试器将不会挂起在SioctlDeviceControl。

延期断点的一个有用的特性就是它们在“模块!名字”处起作用,而普通断点在地址处起作用或把马上把“模块!名字”分解为地址后起作用。延期断点的另一个特性是它们透过引导被记住(因而不会理解地址表达式断点)。还有,如果模块没有被加载,延期断点会被记住,而普通断点将被移去。

设置延期断点的另一种方式是经由源代码窗口。返回到sioctl.sys,当你断在DriverEntry的时候,你可以向下滚动窗口,找到你希望停止的地方,把光标放在希望的行按F9

 

红色的行就是你经由F9设置的断点。

为了看到你定义的所有断点,使用bl(breakpoint list)命令:

kd> bl

 0 e [d:\winddk\3790\src\general\ioctl\sys\sioctl.c @ 123]    0001 (0001) SIoctl!DriverEntry

 1 e [d:\winddk\3790\src\general\ioctl\sys\sioctl.c @ 338]    0001 (0001) Sioctl!SioctlDeviceControl+0x103

注意这几件事:每一个断点有一个编号,断点的状态也被显示出来,“e”标识有效,“d”标识被禁用。

现在假定你希望临时使一个断点暂时不可用,bd(disable breakpoint)可以做到,但你必须指定断点编号。

kd> bd 1

kd> bl

 0 e [d:\winddk\3790\src\general\ioctl\sys\sioctl.c @ 123]    0001 (0001) SIoctl!DriverEntry

 1 d [d:\winddk\3790\src\general\ioctl\sys\sioctl.c @ 338]    0001 (0001) SIoctl!SioctlDeviceControl+0x103

还有一种相似的方法,为了永久移去断点号为1的断点,使用“bc 1”(clear breakpoint),这样那个断点将被从断点列表中移去。

如果一个断点被设置到操作系统或驱动程序非常忙的地方,你也许想在某些条件满足的时候才停止,下面是这种断点的基本形式:

bp SIoctl!SioctlDeviceControl+0x103 "j (@@(Irp)=0xffb5c4f8) ''; 'g'"

这个意思就是说,如果指针Irp等于地址0xFFB5C4F8时才断住,如果条件不满足,则继续运行。

更进一步深入,这个断点不是它自己的条件,该断点有一个动作从句(在双引号里面);这个从句中命令j(执行if、else)是一个有条件的动作,j的功能是执行它的TRUE|FALSE从句(在单引号里)。在上面的TRUE从句(第一个)是空,所以当到达断点并且遇到TRUE条件时,windbg什么也不做而不是缺省的挂起。如果遇到FALSE从句(第二个),执行会继续,也就是执行g。一个动作或其它的什么将会发生,这依赖于条件。

 下面是比上面稍复杂些的:

bp SIoctl!SioctlDeviceControl+0x103 "j (@@(Irp)=0xffb5c4f8) '.echo Found the interesting IRP' ; '.echo Skipping an IRP of no interest; g' "

这里的TRUE从句输出一个消息并且停止。FALSE从句输出一个消息并继续,这里的消息是有用的,因为windbg对条件估值为FALSE就不能保持静默。

注意:下面的断点测试寄存器eax(你可以在registers找到更多完整的寄存器),就像你期望的那样不能工作。

bp SIoctl!SioctlDeviceControl+0x103 "j (@eax=0xffb5c4f8) '.echo Here!' ; '.echo Skipping; g' "

原因是估值可能寄存器的值是64位的,也就是说,0xFFFFFFFF`FFB5C4F8不匹配0x00000000`FFB5C4F8。这只在32位值的最高位是1的时候出现问题,如果某些别的条件(例如,32位寄存器)有符号扩展时出现。符号扩展在windows调试工具帮助文档中给出了有关细节(参见设置条件断点 “Setting a Conditional Breakpoint”)

一个断点可能条件,带有或不带有条件动作。一个条件是一次性的:断点只会被触发一次(击发后被清除)。对仅击发一次这很方便。

bp /1 SIoctl!SioctlDeviceControl+0x103

其它有用的条件是测试一个进程或线程:

bp /p 0x81234000 SIoctl!SioctlDeviceControl+0x103

bp /t 0xff234000 SIoctl!SioctlDeviceControl+0x103

这分别表示,如果进程阻塞在0x81234000,或者线程阻塞在0xFF234000,就停止在标识点。

条件可以联合:

bp /1 /C 4 /p 0x81234000 SIoctl!SioctlDeviceControl+0x103

这意味着,中断一次、但仅当调用堆栈深度大于4(这里大写C是有意义的,因为小写c表示小于)并且进程阻塞在0x81234000。

另一种不同类型的断点是指定访问。例如:

ba w4 0xffb5c4f8+0x18+0x4

上边你看到的地址是IRP的地址,偏移量0x18+0x4处是IRP的IoStatus.Information成员,所以,这个断点将在导致尝试更新IRP的IoStatus.Information的4个字节的时候触发。

表达式:MASM与C++

你可能会同意这个观点,在一个驱动程序中使用一个变量来提供一个诸如进程地址这样的参数值是很方便的,这样的话,说明你了解调试器表达式。

调试器有两种对表达式的估值方式,参见“MASM“(Microsoft Macro Assembler)和“C++”。具体见windows调试工具帮助文档中的“MASM表达式与C++表达式 MASM Expressions vs. C++ Expressions”

在MASM表达式中,任何符号数字值都是它的内存地址。在一个C++表达式中,变量的数字值是它实际的值,而不是它的地址。

读并重读那一节将能很好地报答你以时间。

一个表达式被MASM规则、C++规则或它们的联合,简而言之:

1.    缺省表达式类型是MASM。

2.    缺省类型可以用.Expr改变(参见windows调试工具帮助文档)。

3.    某些命令总是使用C++估值。

4.    特定表达式(或表达式的一部分)的估值可以通过表达式前缀“@@”用通常的表达式类型相反的类型。

尽管这个汇总相当难懂,但你可以参考windows调试工具帮助文档的“表达式估值Evaluating Expressions”获取相关细节。下面是几个例子,演示了估值是怎么工作的。

先前已经停止在Sioctl!SioctlDeviceControl+0x103的地方,所以使用dv来查看一个变量(参见dv命令dv command获取更多的信息)

kd> dv Irp

            Irp = 0xff70fbc0

这个响应表示变量Irp包含0xFF70FBC0,dv用C++方式解释它的参数,也就是说,那是它的内容而不是地址,你可以这么确认:

kd> ?? Irp

struct _IRP * 0xff70fbc0

 

因为??是基于C++的功能(参见??命令 ?? command),而MASM类型的估值使用?(参见?命令 ? command):

kd> ? Irp

Evaluate expression: -141181880 = f795bc48

这个意思是说,变量Irp在0XF795BC48这个地方,这就可以确认那个地方的变量包含的值真的是0xFF70FBC0,通过内存显示命令dd(参见dd command):

kd> dd f795bc48 l1

f795bc48  ff70fbc0

并且那里保存的是:

kd> dd 0xff70fbc0

ff70fbc0  00940006 00000000 00000070 ff660c30

ff70fbd0  ff70fbd0 ff70fbd0 00000000 00000000

ff70fbe0  01010001 04000000 0006fdc0 00000000

ff70fbf0  00000000 00000000 00000000 04008f20

ff70fc00  00000000 00000000 00000000 00000000

ff70fc10  ff73f4d8 00000000 00000000 00000000

ff70fc20  ff70fc30 ffb05b90 00000000 00000000

ff70fc30  0005000e 00000064 0000003c 9c402408

这看起来真的像一个IRP,用dt显示(参见dt command),可以看到类型和大小:

kd> dt Irp

Local var @ 0xf795bc48 Type _IRP*

0xff70fbc0

   +0x000 Type             : 6

   +0x002 Size             : 0x94

   +0x004 MdlAddress       : (null)

   +0x008 Flags            : 0x70

   +0x00c AssociatedIrp    : __unnamed

   +0x010 ThreadListEntry  : _LIST_ENTRY [ 0xff70fbd0 - 0xff70fbd0 ]

   +0x018 IoStatus         : _IO_STATUS_BLOCK

   +0x020 RequestorMode    : 1 ''

   +0x021 PendingReturned  : 0 ''

   +0x022 StackCount       : 1 ''

   +0x023 CurrentLocation  : 1 ''

   +0x024 Cancel           : 0 ''

   +0x025 CancelIrql       : 0 ''

   +0x026 ApcEnvironment   : 0 ''

   +0x027 AllocationFlags  : 0x4 ''

   +0x028 UserIosb         : 0x0006fdc0

   +0x02c UserEvent        : (null)

   +0x030 Overlay          : __unnamed

   +0x038 CancelRoutine    : (null)

   +0x03c UserBuffer       : 0x04008f20

         +0x040 Tail             : __unnamed

有时,你可能想在MASM表达式里面使用C++估值,“@@”前缀实现这个功能。因为扩展总是使用MASM表达式,当一个使用扩展命令!irp的时候你可以看到@@的效果(参见IRPs)。

kd> !irp @@(Irp)

Irp is active with 1 stacks 1 is current (= 0xff70fc30)

 No Mdl System buffer = ff660c30 Thread ff73f4d8:  Irp stack trace. 

     cmd  flg cl Device   File     Completion-Context

>[  e, 0]   5  0 82361348 ffb05b90 00000000-00000000   

       \Driver\SIoctl

         Args: 00000064 0000003c 9c402408 00000000

如果重复一遍而且变量Irp没有@@前缀,!irp使用变量的地址而不是值,关键是如果变量在0xF795BC48并且它的值是0xFF70FBC0,用!irp Irp替代!irp @@(Irp)将使windbg在0xF795BC48格式化IRP堆栈。

另外,@@前缀还不止这些,使用估值方法时,使用MASM估值@@意味着C++,相反,C++估值时@@意味着MASM。

最后,如果你的表达式没有按你期望的估值,就要考虑调试器是用MASM还是C++估值。

显示并设置内存、变量、寄存器等等

有相当多的方法来显示或改变它们。

  • 为了显示当前程序中的变量(当前作用域),使用dv(显示变量 Display Variables),例如,如果停止在Sioctl!SioctlDeviceControl+0x103:

kd> dv

   DeviceObject = 0x82361348

            Irp = 0xff70fbc0

   outBufLength = 0x64

         buffer = 0x00000000 ""

          irpSp = 0xff70fc30

           data = 0xf886b0c0 "This String is from Device Driver !!!"

       ntStatus = 0

            mdl = 0x00000000

    inBufLength = 0x3c

        datalen = 0x26

         outBuf = 0x00000030 ""

          inBuf = 0xff660c30 "This String is from User Application; using METHOD_BUFFERED"

这是一个该断点“知道的”参数和局部变量列表,所谓知道是一个重要的修饰语。例如,如果一个变脸被优化到一个寄存器中,它将不被显示,尽管反汇编可以看到寄存器(View-Disassembly可以打开反汇编窗)。

如果关心的仅仅是一个变量,你可以这样:

kd> dv outBufLength

   outBufLength = 0x64

  • 另一个有用的命令是dt(“Display Type”)。例如,继续使用Sioctl!SioctlDeviceControl+0x103这个断点:

kd> dt Irp

Local var @ 0xf795bc48 Type _IRP*

0xff70fbc0

   +0x000 Type             : 6

   +0x002 Size             : 0x94

   +0x004 MdlAddress       : (null)

   +0x008 Flags            : 0x70

   +0x00c AssociatedIrp    : __unnamed

   +0x010 ThreadListEntry  : _LIST_ENTRY [ 0xff70fbd0 - 0xff70fbd0 ]

   +0x018 IoStatus         : _IO_STATUS_BLOCK

   +0x020 RequestorMode    : 1 ''

   +0x021 PendingReturned  : 0 ''

   +0x022 StackCount       : 1 ''

   +0x023 CurrentLocation  : 1 ''

   +0x024 Cancel           : 0 ''

   +0x025 CancelIrql       : 0 ''

   +0x026 ApcEnvironment   : 0 ''

   +0x027 AllocationFlags  : 0x4 ''

   +0x028 UserIosb         : 0x0006fdc0

   +0x02c UserEvent        : (null)

   +0x030 Overlay          : __unnamed

   +0x038 CancelRoutine    : (null)

   +0x03c UserBuffer       : 0x04008f20

   +0x040 Tail             : __unnamed

上面是说Irp是在0xF795BC48,并且包含了值0xFF70FBC0,因为dt知道变量一个IRP指针(“Type _IRP*”),0xFF70FBC0这个区域被格式化成一个IRP。

为了展开这个结构的任何一级:

kd> dt -r1 Irp

Local var @ 0xf795bc48 Type _IRP*

0xff70fbc0

   +0x000 Type             : 6

   +0x002 Size             : 0x94

   +0x004 MdlAddress       : (null)

   +0x008 Flags            : 0x70

   +0x00c AssociatedIrp    : __unnamed

      +0x000 MasterIrp        : 0xff660c30

      +0x000 IrpCount         : -10089424

      +0x000 SystemBuffer     : 0xff660c30

   +0x010 ThreadListEntry  : _LIST_ENTRY [ 0xff70fbd0 - 0xff70fbd0 ]

      +0x000 Flink            : 0xff70fbd0  [ 0xff70fbd0 - 0xff70fbd0 ]

      +0x004 Blink            : 0xff70fbd0  [ 0xff70fbd0 - 0xff70fbd0 ]

   +0x018 IoStatus         : _IO_STATUS_BLOCK

      +0x000 Status           : 0

      +0x000 Pointer          : (null)

      +0x004 Information      : 0

   +0x020 RequestorMode    : 1 ''

   +0x021 PendingReturned  : 0 ''

   +0x022 StackCount       : 1 ''

   +0x023 CurrentLocation  : 1 ''

   +0x024 Cancel           : 0 ''

   +0x025 CancelIrql       : 0 ''

   +0x026 ApcEnvironment   : 0 ''

   +0x027 AllocationFlags  : 0x4 ''

   +0x028 UserIosb         : 0x0006fdc0

      +0x000 Status           : 67142040

      +0x000 Pointer          : 0x04008198

      +0x004 Information      : 0x2a

   +0x02c UserEvent        : (null)

   +0x030 Overlay          : __unnamed

      +0x000 AsynchronousParameters : __unnamed

      +0x000 AllocationSize   : _LARGE_INTEGER 0x0

   +0x038 CancelRoutine    : (null)

   +0x03c UserBuffer       : 0x04008f20

   +0x040 Tail             : __unnamed

      +0x000 Overlay          : __unnamed

      +0x000 Apc              : _KAPC

      +0x000 CompletionKey    : (null)

甚至那些不在当前范围的结构也能显示(假定那些内存已经被用做其它用途了):

kd> dt nt!_IRP 0xff70fbc0

   +0x000 Type             : 6

   +0x002 Size             : 0x94

   +0x004 MdlAddress       : (null)

   +0x008 Flags            : 0x70

   +0x00c AssociatedIrp    : __unnamed

   +0x010 ThreadListEntry  : _LIST_ENTRY [ 0xff70fbd0 - 0xff70fbd0 ]

   +0x018 IoStatus         : _IO_STATUS_BLOCK

   +0x020 RequestorMode    : 1 ''

   +0x021 PendingReturned  : 0 ''

   +0x022 StackCount       : 1 ''

   +0x023 CurrentLocation  : 1 ''

   +0x024 Cancel           : 0 ''

   +0x025 CancelIrql       : 0 ''

   +0x026 ApcEnvironment   : 0 ''

   +0x027 AllocationFlags  : 0x4 ''

   +0x028 UserIosb         : 0x0006fdc0

   +0x02c UserEvent        : (null)

   +0x030 Overlay          : __unnamed

   +0x038 CancelRoutine    : (null)

   +0x03c UserBuffer       : 0x04008f20

   +0x040 Tail             : __unnamed

上面这个命令丰富了你的知识-在0xFF70FBC0有一个IRP并且事实上在ntoskrnl里存在一个IRP结构的映射。

  • 如果你对有很多成员的结构体的某个成员感兴趣又该怎么办呢?得到成员的大小,例如:

kd> dt nt!_IRP Size 0xff70fbc0

unsigned short 0x94

更直觉的方式是??(估值一个C++表达式)命令:

kd> ?? Irp->Size

unsigned short 0x94

??了解它的参数是某个结构体的成员指针。

  • 不用上面格式的命令也能显示内存,就像dd、dw、db这些命令(“Display Memory”):

kd> dd 0xff70fbc0 l0x10

ff70fbc0  00940006 00000000 00000070 ff660c30

ff70fbd0  ff70fbd0 ff70fbd0 00000000 00000000

ff70fbe0  01010001 04000000 0006fdc0 00000000

ff70fbf0  00000000 00000000 00000000 04008f20

 

kd> dw 0xff70fbc0 l0x20

ff70fbc0  0006 0094 0000 0000 0070 0000 0c30 ff66

ff70fbd0  fbd0 ff70 fbd0 ff70 0000 0000 0000 0000

ff70fbe0  0001 0101 0000 0400 fdc0 0006 0000 0000

ff70fbf0  0000 0000 0000 0000 0000 0000 8f20 0400

 

kd> db 0xff70fbc0 l0x40

ff70fbc0  06 00 94 00 00 00 00 00-70 00 00 00 30 0c 66 ff  ........p...0.f.

ff70fbd0  d0 fb 70 ff d0 fb 70 ff-00 00 00 00 00 00 00 00  ..p...p.........

ff70fbe0  01 00 01 01 00 00 00 04-c0 fd 06 00 00 00 00 00  ................

ff70fbf0  00 00 00 00 00 00 00 00-00 00 00 00 20 8f 00 04  ............ ...

(注意:上面3个命令的第二个参数是一个长度,跟着0x10这样的值之后用l (字母“l”)直接给出)。

第一个显示16个双字(每个4个字节,总计64个字节),第二个用字显示相同的内容,第三个则是用字节方式显示。

  • 改变一个变量又该怎么办呢?还是Sioctl!SioctlDeviceControl+0x103,你将直觉地看到:

kd> outBufLength = 00

     ^ Syntax error in 'outBufLength = 00'

这样不行,用??可以做到:

kd> ?? outBufLength = 0

unsigned long 0

现在回到你使用的IRP,用上面讲的dt:

kd> dt Irp

Local var @ 0xf795bc48 Type _IRP*

0xff70fbc0

   +0x000 Type             : 6

   +0x002 Size             : 0x94

   +0x004 MdlAddress       : (null)

   +0x008 Flags            : 0x70

   +0x00c AssociatedIrp    : __unnamed

   +0x010 ThreadListEntry  : _LIST_ENTRY [ 0xff70fbd0 - 0xff70fbd0 ]

   +0x018 IoStatus         : _IO_STATUS_BLOCK

   +0x020 RequestorMode    : 1 ''

   +0x021 PendingReturned  : 0 ''

   +0x022 StackCount       : 1 ''

   +0x023 CurrentLocation  : 1 ''

   +0x024 Cancel           : 0 ''

   +0x025 CancelIrql       : 0 ''

   +0x026 ApcEnvironment   : 0 ''

   +0x027 AllocationFlags  : 0x4 ''

   +0x028 UserIosb         : 0x0006fdc0

   +0x02c UserEvent        : (null)

   +0x030 Overlay          : __unnamed

   +0x038 CancelRoutine    : (null)

   +0x03c UserBuffer       : 0x04008f20

   +0x040 Tail             : __unnamed

用ew(“Enter Values”)改变第一个字(两个字节):

kd> ew 0xff70fbc0 3

 

kd> dt Irp

Local var @ 0xf795bc48 Type _IRP*

0xff70fbc0

   +0x000 Type             : 3

   +0x002 Size             : 0x94

   +0x004 MdlAddress       : (null)

   +0x008 Flags            : 0x70

   +0x00c AssociatedIrp    : __unnamed

   +0x010 ThreadListEntry  : _LIST_ENTRY [ 0xff70fbd0 - 0xff70fbd0 ]

   +0x018 IoStatus         : _IO_STATUS_BLOCK

   +0x020 RequestorMode    : 1 ''

   +0x021 PendingReturned  : 0 ''

   +0x022 StackCount       : 1 ''

   +0x023 CurrentLocation  : 1 ''

   +0x024 Cancel           : 0 ''

   +0x025 CancelIrql       : 0 ''

   +0x026 ApcEnvironment   : 0 ''

   +0x027 AllocationFlags  : 0x4 ''

   +0x028 UserIosb         : 0x0006fdc0

   +0x02c UserEvent        : (null)

   +0x030 Overlay          : __unnamed

   +0x038 CancelRoutine    : (null)

   +0x03c UserBuffer       : 0x04008f20

   +0x040 Tail             : __unnamed

当然,下面的可能比ew更自然:

kd> ?? irp->type = 3

Type does not have given member error at 'type = 3'

kd> ?? irp->Type = 3

short 3

 

kd> dt irp

ioctlapp!Irp

Local var @ 0xf795bc48 Type _IRP*

0xff70fbc0

   +0x000 Type             : 3

   +0x002 Size             : 0x94

   +0x004 MdlAddress       : (null)

   +0x008 Flags            : 0x70

   +0x00c AssociatedIrp    : __unnamed

   +0x010 ThreadListEntry  : _LIST_ENTRY [ 0xff70fbd0 - 0xff70fbd0 ]

   +0x018 IoStatus         : _IO_STATUS_BLOCK

   +0x020 RequestorMode    : 1 ''

   +0x021 PendingReturned  : 0 ''

   +0x022 StackCount       : 1 ''

   +0x023 CurrentLocation  : 1 ''

   +0x024 Cancel           : 0 ''

   +0x025 CancelIrql       : 0 ''

   +0x026 ApcEnvironment   : 0 ''

   +0x027 AllocationFlags  : 0x4 ''

   +0x028 UserIosb         : 0x0006fdc0

   +0x02c UserEvent        : (null)

   +0x030 Overlay          : __unnamed

   +0x038 CancelRoutine    : (null)

   +0x03c UserBuffer       : 0x04008f20

   +0x040 Tail             : __unnamed

上面有几个问题要注意,首先,结构体成员大小写敏感,就像WinDbg上面报告的,irp没有type成员;其次,dt irp是有歧义的,但是WinDbg会显示它认为适当的那个,ioctlapp.exe里的或sioctl.sys里的。因为大小写敏感,你应该使用你了解的那个。

除ew之外,还有其它的输入值的命令:eb用于字节,ed用于双字,eq用于四字(8个字节)等等,参见windows调试工具帮助文档的“输入值Enter Values”。

  • 在局部变量窗你可以更容易使用嵌入一个指向结构的结构,你可以在这里改变变量的值 :

 

  • 寄存器(也包括段指针和标志)可以被显示和修改。例如:

kd> r

eax=81478f68 ebx=00000000 ecx=814243a8 edx=0000003c esi=81778ea0 edi=81478f68

eip=f8803553 esp=f7813bb4 ebp=f7813c3c iopl=0         nv up ei ng nz ac pe nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000292

或仅仅这样:

kd> r eax

eax=81478f68

有时将可能想修改一个寄存器。例如Eax就经常被用来保存程序退出的返回值,所以,在程序退出前这样:

r eax = 0xc0000001

这将使状态值为STATUS_UNSUCCESSFUL。

这里还有其它的一些例子:

r eip = poi(@esp)

r esp = @esp + 0xc

它们分别表示设置Eip(指令指针)为堆栈的0偏移量的地方、把Esp加上0xc使堆栈后退。Windows调试工具文档的“寄存器语法Register Syntax”解释了poi以及为什么寄存器名在有些地方要有一个@前缀。

你可能会问,上面的寄存器设置命令有什么用。考虑这么一种情况:有一个“坏的”驱动程序的DriverEntry将导致一个bug检查(蓝屏)--由于一个访问违例,也许当ntoskrnl加载的时候你可以通过设置一个延期断点来处理这种情况:

bu sioctl!DriverEntry "r eip = poi(@esp); r eax = 0xc0000001; r esp = @esp + 0xc; .echo sioctl!DriverEntry entered; g"

这个意思是:在sioctl.sys的DriverEntry函数里1)设置指令指针(Eip),2)设置返回码(eax),3)设置堆栈指针(Esp),4)显示该函数进入通知,5)继续运行。(当然,这个技巧仅仅是为了模拟DriverEntry出现内存访问违例之类的异常。如果操作系统期望该驱动程序函数被调用,就使该函数不可用,并且下一步可能会有其他问题。)

假使你疑惑使用一个寄存器来设置一个变量是否可行,答案是肯定的。例如,还是回到IoCtl派遣例程:

kd> r

eax=00000000 ebx=00000000 ecx=81a88f18 edx=81a88ef4 esi=ff9e18a8 edi=ff981e7e

eip=f87a40fe esp=f88fac78 ebp=f88fac90 iopl=0         nv up ei pl zr na po nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246

 

kd> ?? ntStatus = @ecx

long -2119659752

kd> dd &ntStatus l1

f88fac78  81a88f18

在这种情况下,应该使用@ecx形式以确保WinDbg知道你在引用一个寄存器。

除了显示的寄存器,还有更多的寄存器。为了看到全部,使用rM命令(M必须大写,它实际是r命令的参数,但是之间没有空格,这时允许的。

kd> rM 0xff

eax=00000001 ebx=0050e2a3 ecx=80571780 edx=000003f8 esi=000000c0 edi=d87a75a8

eip=804df1c0 esp=8056f564 ebp=8056f574 iopl=0         nv up ei pl nz na pe nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000202

fpcw=0000: rn 24 ------  fpsw=0000: top=0 cc=0000 --------  fptw=0000

fopcode=6745  fpip=2301:a0020000  fpdp=dcfe:efcdab89

st0= 5.143591243081972142170e-4932  st1= 0.001025530551233493990e-4933

st2= 0.000000002357022271740e-4932  st3= 2.471625214254630491460e-4906

st4= 3.370207406893238285120e-4932  st5=-7.461339669368745455450e+4855

st6= 6.698191557136036873700e-4932  st7=-2.455410815115332972380e-4906

mm0=c3d2e1f010325476  mm1=0000ffdff1200000

mm2=000000018168d902  mm3=f33cffdff1200000

mm4=804efc868056f170  mm5=7430804efb880000

mm6=ff02740200000000  mm7=f1a48056f1020000

xmm0=0 9.11671e-041 3.10647e+035 -1.154e-034

xmm1=-7.98492e-039 -2.83455e+038 -2.91106e+038 5.85182e-042

xmm2=1.77965e-043 -1.17906e-010 -4.44585e-038 -7.98511e-039

xmm3=-7.98511e-039 0 0 -7.98504e-039

xmm4=-7.98503e-039 1.20545e-040 -1.47202e-037 -1.47202e-037

xmm5=-2.05476e+018 -452.247 -1.42468e-037 -8.60834e+033

xmm6=2.8026e-044 -1.47202e-037 -452.247 0

xmm7=8.40779e-045 -7.98503e-039 0 -7.98511e-039

cr0=8001003b cr2=d93db000 cr3=00039000

dr0=00000000 dr1=00000000 dr2=00000000

dr3=00000000 dr6=ffff0ff0 dr7=00000400 cr4=000006d9

  • 如果你不喜欢使用命令来改变,你还可以使用内存窗(ViewàMemory)、变量窗(ViewàLocals)或寄存器窗(ViewàRegisters)来修改值。例如:

 

控制执行

先前(参见IoCreateDevice),你曾被告知让进程从一个点执行到下一个点,但没有告诉你怎么做。有几种方式可以控制进程。下面的方法除了第一个都假定当前是在挂起状态。

  • 中断执行(CTRL-BREAK)—这个键盘序列总是中断系统,只要系统正在运行,并且与WinDbg正在通讯状态。
  • 单步执行(F10)—这会导致一次执行一个语句(if是C或C++而且WinDbg是处于源代码模式,菜单为DebugàSource Mode)或一个指令。
  • 单步进入(F11)—这就像单步执行一样,只是会进入被调用的例程中执行。
  • 单步跳出(SHIFT-F11)—跳出当前执行的例程,回到调用堆栈的上一级。这在你看到太多例程的时候很有用。

运行到光标处(F7 or CRTL-F10)—把光标放到源代码或反汇编窗中你想断住的某个地方,按F7,程序将执行到那个地方。不过,如果程序流无法到达那个地方(例如,一个不被执行的if语句里),WinDbg将不会断住,因为执行没有走到标识的那个点。

  • 运行(F5)—一直运行直到遇到一个断点或一个类似bug检查的事件发生。你可能想像正常执行的方式运行。
  • 设置指令到当前行(CTRL-SHIFT-I)—在源代码窗,你可以把光标放到某一行,按下这个组合键,像你让它从那个点开始执行一样(例如,F5或F10)。如果你想要重新执行一个序列,这很方便。但是,这需要格外小心,例如,寄存器和变量没有被设置成它正常运行到那处时的值。
  • 直接设置Eip—你可以把值放入Eip寄存器,然后按F5(或F10或其它),执行将从那个地址开始。显然,这类似于设置指令到光标指定的当前行,除非你指定一个汇编指令的地址。

调用堆栈

在执行的几乎所有点,都有一个区域被用做堆栈,这个堆栈是用来保存局部状态的,包括参数和返回地址。如果执行在内核空间,就有一个内核堆栈,如果执行在用户空间,就有一个用户堆栈。当你命中一个断点的时候,当前堆栈中可能会有好几个程序,也可能会有相当多。例如,如果指令停止在程序sioctl.sys的PrintIrpInfo函数中,使用k(“Stack Backtrace”):

kd> k

ChildEBP RetAddr 

f7428ba8 f889b54a SIoctl!PrintIrpInfo+0x6 [d:\winddk\3790.1824\src\general\ioctl\sys\sioctl.c @ 708]

f7428c3c 804e0e0d SIoctl!SioctlDeviceControl+0xfa [d:\winddk\3790.1824\src\general\ioctl\sys\sioctl.c @ 337]

WARNING: Stack unwind information not available. Following frames may be wrong.

f7428c60 80580e2a nt!IofCallDriver+0x33

f7428d00 805876c2 nt!CcFastCopyRead+0x3c3

f7428d34 804e7a8c nt!NtDeviceIoControlFile+0x28

f7428d64 00000000 nt!ZwYieldExecution+0xaa9

最上面的一行(最新的)是控制被停止的堆栈帧。你也可以看到之前的调用者,但是,如果你没有符号,它们就无法被正确地表现出来。

你可以方便地切换到IoCtl的IRP处理程序所在的源代码窗,但是要是你想看更早的程序呢?你激活调用堆栈窗(ViewàCall stack):

 

你可以双击一项能显示源文件,如果源文件能被定位的话。

如果你只对一个程序内部的变量感兴趣,你可以通过双击上面的它那一行,或用kn(k的兄弟),然后再用.frame。例如,得到调用PrintIrpInfo的派遣例程的信息:

kd> kn

 # ChildEBP RetAddr 

00 f7428ba8 f889b54a SIoctl!PrintIrpInfo+0x6 [d:\winddk\3790.1824\src\general\ioctl\sys\sioctl.c @ 708]

01 f7428c3c 804e0e0d SIoctl!SioctlDeviceControl+0xfa [d:\winddk\3790.1824\src\general\ioctl\sys\sioctl.c @ 337]

WARNING: Stack unwind information not available. Following frames may be wrong.

02 f7428c60 80580e2a nt!IofCallDriver+0x33

03 f7428d00 805876c2 nt!CcFastCopyRead+0x3c3

04 f7428d34 804e7a8c nt!NtDeviceIoControlFile+0x28

05 f7428d64 00000000 nt!ZwYieldExecution+0xaa9

kd> .frame 1

01 f7428c3c 804e0e0d SIoctl!SioctlDeviceControl+0xfa [d:\winddk\3790.1824\src\general\ioctl\sys\sioctl.c @ 337]

设置帧号你就能显示(或修改,如果你希望的话)在那个帧中已知的变量和属于那一帧的寄存器。

kd> dv

   DeviceObject = 0x80f895e8

            Irp = 0x820572a8

   outBufLength = 0x64

         buffer = 0x00000000 ""

          irpSp = 0x82057318

           data = 0xf889b0c0 "This String is from Device Driver !!!"

       ntStatus = 0

            mdl = 0x00000000

    inBufLength = 0x3c

        datalen = 0x26

         outBuf = 0x82096b20 "This String is from User Application; using METHOD_BUFFERED"

          inBuf = 0x82096b20 "This String is from User Application; using METHOD_BUFFERED"

kd> r

eax=00000000 ebx=00000000 ecx=80506be8 edx=820572a8 esi=81fabda0 edi=820572a8

eip=f889bcf6 esp=f7428ba4 ebp=f7428ba8 iopl=0         nv up ei ng nz ac pe nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000292

SIoctl!PrintIrpInfo+0x6:

f889bcf6 8b4508           mov     eax,[ebp+0x8]     ss:0010:f7428bb0=820572a8

找到一个模块的名字

x(“检查符号 Examine Symbols”)命令定位模块中的符号。例如,如果你想在Ioctl程序中放一个断点来处理DeviceIoControl的IRP,但是不完全记得程序的名字了,可以这么做:

kd> x sioctl!*ioctl*

f8883080 SIoctl!SioctlUnloadDriver (struct _DRIVER_OBJECT *)

f8883010 SIoctl!SioctlCreateClose (struct _DEVICE_OBJECT *, struct _IRP *)

f8883450 SIoctl!SioctlDeviceControl (struct _DEVICE_OBJECT *, struct _IRP *)

这个意思是告诉你模块sioctl中的所有包含ioctl的符号。

可能会比较琐碎,可是在调试器里经常遇到这样的情况:

PopPolicyWorkerAction: action request 2 failed c000009a

估计PopPolicyWorkerAction程序是在ntoskrnl中,你可以看看那里:

kd> x nt!PopPolicy*

805146c0 nt!PopPolicyWorkerThread = <no type information>

8064e389 nt!PopPolicySystemIdle = <no type information>

805b328d nt!PopPolicyWorkerNotify = <no type information>

8056e620 nt!PopPolicyLock = <no type information>

8064d5f8 nt!PopPolicyWorkerActionPromote = <no type information>

805c7d10 nt!PopPolicyWorkerMain = <no type information>

8064d51b nt!PopPolicyWorkerAction = <no type information>

80561c70 nt!PopPolicy = <no type information>

8056e878 nt!PopPolicyIrpQueue = <no type information>

80561a98 nt!PopPolicyLockThread = <no type information>

8064e74a nt!PopPolicyTimeChange = <no type information>

8056e8b0 nt!PopPolicyWorker = <no type information>

上面的信息中,你可以在显示为红色的程序中放一个断点。

Dealing with optimized code

处理优化代码

如果一个可执行文件被构建成发行版本,并且/或者进行了某些优化,它执行时可能与源代码窗中显示的不同,并且局部变量可能不可用,或者显示不正确的值。x86代码你可能尝试在源代码窗和反汇编窗中顺序执行(最好挨着放这些窗口)。你不得不了解非常多的关于x86汇编来关注控制流,重点关注测试(比如,test或cmp)和分支(比如,jnz)指令以便跟踪控制流。

选择技巧

前面是基本操作,但是并不包含怎样检视特定的方面,还有许多实际的调试命令—事实上它们是写成动态库的扩展—也被称为技巧,因为它们常被用于方方面面,值得说说。

进程和线程

为了看当前进程(在一个断住的点上):

kd> !process

PROCESS 816fc3c0  SessionId: 1  Cid: 08f8    Peb: 7ffdf000  ParentCid: 0d8c

    DirBase: 10503000  ObjectTable: e1afeaa8  HandleCount:  19.

    Image: ioctlapp.exe

    VadRoot 825145e0 Vads 22 Clone 0 Private 38. Modified 0. Locked 0.

    DeviceMap e10d0198

    Token                             e1c8e030

    ElapsedTime                       00:00:00.518

    UserTime                          00:00:00.000

    KernelTime                        00:00:00.109

    QuotaPoolUsage[PagedPool]         9096

    QuotaPoolUsage[NonPagedPool]      992

    Working Set Sizes (now,min,max)  (263, 50, 345) (1052KB, 200KB, 1380KB)

    PeakWorkingSetSize                263

    VirtualSize                       6 Mb

    PeakVirtualSize                   6 Mb

    PageFaultCount                    259

    MemoryPriority                    BACKGROUND

    BasePriority                      8

    CommitCharge                      48

 

        THREAD 825d2020  Cid 08f8.0708  Teb: 7ffde000 Win32Thread: 00000000 RUNNING on processor 0

进程块(EPROCESS)和线程块(ETHREAD)地址被标为红色。你在条件断点中可以使用它们。

为了看到所有的进程的汇总:

kd> !process 0 0

**** NT ACTIVE PROCESS DUMP ****

PROCESS 826af478  SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000

    DirBase: 02c20000  ObjectTable: e1001e60  HandleCount: 363.

    Image: System

 

PROCESS 82407d88  SessionId: none  Cid: 0158    Peb: 7ffdf000  ParentCid: 0004

    DirBase: 1fbe8000  ObjectTable: e13ff740  HandleCount:  24.

    Image: smss.exe

 

PROCESS 82461d88  SessionId: 0  Cid: 0188    Peb: 7ffdf000  ParentCid: 0158

    DirBase: 1f14d000  ObjectTable: e15e8958  HandleCount: 408.

    Image: csrss.exe

 

...

为了看到一个特定进程的的线程汇总,给出进程块地址,经由第二个参数请求更多细节(参见windows调试工具帮助文档关于参数的细节):

kd> !process 826af478 3

PROCESS 826af478  SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000

    DirBase: 02c20000  ObjectTable: e1001e60  HandleCount: 362.

    Image: System

    VadRoot 81a43840 Vads 4 Clone 0 Private 3. Modified 18884. Locked 0.

    DeviceMap e1002868

    Token                             e1002ae0

    ElapsedTime                       07:19:11.250

    UserTime                          00:00:00.000

    KernelTime                        00:00:11.328

    QuotaPoolUsage[PagedPool]         0

    QuotaPoolUsage[NonPagedPool]      0

    Working Set Sizes (now,min,max)  (54, 0, 345) (216KB, 0KB, 1380KB)

    PeakWorkingSetSize                497

    VirtualSize                       1 Mb

    PeakVirtualSize                   2 Mb

    PageFaultCount                    4179

    MemoryPriority                    BACKGROUND

    BasePriority                      8

    CommitCharge                      7

 

        THREAD 826af1f8  Cid 0004.0008  Teb: 00000000 Win32Thread: 00000000 WAIT: (WrFreePage) KernelMode Non-Alertable

            80580040  SynchronizationEvent

            80581140  NotificationTimer

 

        THREAD 826aea98  Cid 0004.0010  Teb: 00000000 Win32Thread: 00000000 WAIT: (WrQueue) KernelMode Non-Alertable

            80582d80  QueueObject

 

        THREAD 826ae818  Cid 0004.0014  Teb: 00000000 Win32Thread: 00000000 WAIT: (WrQueue) KernelMode Non-Alertable

            80582d80  QueueObject

 

...

为了看特定线程所有尽可能多的细节,用!thread带0xFF作为细节参数:

kd> !thread 826af1f8 0xff

THREAD 826af1f8  Cid 0004.0008  Teb: 00000000 Win32Thread: 00000000 WAIT: (WrFreePage) KernelMode Non-Alertable

    80580040  SynchronizationEvent

    80581140  NotificationTimer

Not impersonating

DeviceMap                 e1002868

Owning Process            826af478       Image:         System

Wait Start TickCount      1688197        Ticks: 153 (0:00:00:02.390)

Context Switch Count      9133            

UserTime                  00:00:00.0000

KernelTime                00:00:03.0406

Start Address nt!Phase1Initialization (0x806fb790)

Stack Init f88b3000 Current f88b2780 Base f88b3000 Limit f88b0000 Call 0

Priority 0 BasePriority 0 PriorityDecrement 0

ChildEBP RetAddr 

f88b2798 804edb2b nt!KiSwapContext+0x26 (FPO: [EBP 0xf88b27c0] [0,0,4])

f88b27c0 804f0e7a nt!KiSwapThread+0x280 (FPO: [Non-Fpo]) (CONV: fastcall)

f88b27f4 80502fc2 nt!KeWaitForMultipleObjects+0x324 (FPO: [Non-Fpo]) (CONV: stdcall)

驱动和设备对象

如果你写了一个驱动程序,你经常要查看设备堆栈。你可能通过查找属于某个驱动的设备,然后仔细检查这个堆栈里的设备。假如你对ScsiPort微端口驱动aic78xx.sys感兴趣,执行!drvobj

kd> !drvobj aic78xx

Driver object (82627250) is for:

 \Driver\aic78xx

Driver Extension List: (id , addr)

(f8386480 8267da38) 

Device Object list:

82666030  8267b030  8263c030  8267ca40

有四个设备对象在这里,用!drvobj查找第一个,获取关于这个设备的信息,用!devstack显示设备所属的设备对象堆栈:

kd> !devobj 82666030 

Device object (82666030) is for:

 aic78xx1Port2Path0Target1Lun0 \Driver\aic78xx DriverObject 82627250

Current Irp 00000000 RefCount 0 Type 00000007 Flags 00001050

Dacl e13bb39c DevExt 826660e8 DevObjExt 82666d10 Dope 8267a9d8 DevNode 8263cdc8

ExtensionFlags (0000000000) 

AttachedDevice (Upper) 826bb030 \Driver\Disk

Device queue is not busy.

kd> !devstack 82666030 

  !DevObj   !DrvObj            !DevExt   ObjectName

  826bbe00  \Driver\PartMgr    826bbeb8 

  826bb030  \Driver\Disk       826bb0e8  DR2

> 82666030  \Driver\aic78xx    826660e8  aic78xx1Port2Path0Target1Lun0

!DevNode 8263cdc8 :

  DeviceInst is "SCSI\Disk&Ven_QUANTUM&Prod_VIKING_II_4.5WLS&Rev_5520\5&375eb691&1&010"

  ServiceName is "disk"

I/O请求包

围绕驱动程序的最通用的通讯方法是I/O请求包,或称IRP。为了看IRP的I/O完成堆栈,看例子Sioctl!SioctlDeviceControl+0x103:

kd> !irp @@(Irp)

Irp is active with 1 stacks 1 is current (= 0xff70fc30)

 No Mdl System buffer = ff660c30 Thread ff73f4d8:  Irp stack trace. 

     cmd  flg cl Device   File     Completion-Context

>[  e, 0]   5  0 82361348 ffb05b90 00000000-00000000   

       \Driver\SIoctl

           Args: 00000064 0000003c 9c402408 00000000

为了得到完整的IRP和它的堆栈,请求更多的细节:

kd> !irp @@(Irp) 1

Irp is active with 1 stacks 1 is current (= 0xff70fc30)

 No Mdl System buffer = ff660c30 Thread ff73f4d8:  Irp stack trace. 

Flags = 00000070

ThreadListEntry.Flink = ff70fbd0

ThreadListEntry.Blink = ff70fbd0

IoStatus.Status = 00000000

IoStatus.Information = 00000000

RequestorMode = 00000001

Cancel = 00

CancelIrql = 0

ApcEnvironment = 00

UserIosb = 0006fdc0

UserEvent = 00000000

Overlay.AsynchronousParameters.UserApcRoutine = 00000000

Overlay.AsynchronousParameters.UserApcContext = 00000000

Overlay.AllocationSize = 00000000 - 00000000

CancelRoutine = 00000000

UserBuffer = 04008f20

&Tail.Overlay.DeviceQueueEntry = ff70fc00

Tail.Overlay.Thread = ff73f4d8

Tail.Overlay.AuxiliaryBuffer = 00000000

Tail.Overlay.ListEntry.Flink = 00000000

Tail.Overlay.ListEntry.Blink = 00000000

Tail.Overlay.CurrentStackLocation = ff70fc30

Tail.Overlay.OriginalFileObject = ffb05b90

Tail.Apc = 00000000

Tail.CompletionKey = 00000000

     cmd  flg cl Device   File     Completion-Context

>[  e, 0]   5  0 82361348 ffb05b90 00000000-00000000   

       \Driver\SIoctl

        Args: 00000064 0000003c 9c402408 00000000

如果仅仅要得到IRP的第一层成员:

kd> dt nt!_IRP @@(Irp)

   +0x000 Type             : 6

   +0x002 Size             : 0x94

   +0x004 MdlAddress       : (null)

   +0x008 Flags            : 0x70

   +0x00c AssociatedIrp    : __unnamed

   +0x010 ThreadListEntry  : _LIST_ENTRY [ 0xff70fbd0 - 0xff70fbd0 ]

   +0x018 IoStatus         : _IO_STATUS_BLOCK

   +0x020 RequestorMode    : 1 ''

   +0x021 PendingReturned  : 0 ''

   +0x022 StackCount       : 1 ''

   +0x023 CurrentLocation  : 1 ''

   +0x024 Cancel           : 0 ''

   +0x025 CancelIrql       : 0 ''

   +0x026 ApcEnvironment   : 0 ''

   +0x027 AllocationFlags  : 0x4 ''

   +0x028 UserIosb         : 0x0006fdc0

   +0x02c UserEvent        : (null)

   +0x030 Overlay          : __unnamed

   +0x038 CancelRoutine    : (null)

   +0x03c UserBuffer       : 0x04008f20

   +0x040 Tail             : __unnamed

IRQL

中断请求级别

一个命令(目标机是Windows Server 2003或以后版本可用)偶尔会用到!irql,它用来显示当前处理器的当前IRQL。在Sioctl!SioctlDeviceControl+0x0处断住:

kd> !irql

Debugger saved IRQL for processor 0x0 -- 0 (LOW_LEVEL)

给一个高IRQL的例子,假定你在Sioctl!SioctlDeviceControl中断点语句之前IOCTL_SIOCTL_METHOD_BUFFERED从句之后添加下面的代码,

Irp->IoStatus.Information = (outBufLength<datalen?outBufLength:datalen);

{                                           

 KIRQL saveIrql;                           

 ULONG i = 0;                              

                                            

 KeRaiseIrql(DISPATCH_LEVEL, &saveIrql);   

 i++;                                      

 KeLowerIrql(saveIrql);                    

}

break;                                          

现在,如果你设置断点在语句KeRaiseIrql之后,命中那个断点:

kd> !irql

Debugger saved IRQL for processor 0x0 -- 2 (DISPATCH_LEVEL)

注意,通过这种方式,!pcr命令不会像通常那样显示你感兴趣的IRQL,也就是,在断点导致挂起的地方的IRQL。

转储文件

转储文件所特有的地方我们很少说起,下面是几件事值得说说。

  • 有三种转储文件。一个全内存转储是最好的,但是稍微小一点的内核转储对大多数目的来说就足够了。还有一种小内存转储,64k大小(因此比前两种生成得更快)。因为小内存转储没有关于可执行文件的全部信息,如果需要检查他们可能需要用.exepath命令指向可执行映像。Windows可以配置成在崩溃时创建这些转储文件中的一种。
  • 为了检查一个转储文件,启动WinDbg但不指定连接目标机使用的协议。在WinDbg中,使用FileàOpen Crash Dump打开转储文件,最好符号和源文件路径已经设置好了。
  • 现在,在WinDbg命令窗中输入!analyze –v,可以得到一个概要。可能会建议你获取执行上下文(.cxr);设置上下文你就可以访问崩溃发生时的调用堆栈(顺利的话,将接近实际的错误)。你可能需要看看进程和线程(!process 和!thread),查看内核模块的列表(lmnt),从那个列表来查看被选择的驱动对象(!drvobj)和可能的设备节点(!devnode)、设备对象(!devobj)和设备堆栈(!devstack)。但是,使用!analyze –v通过转储文件进行工作没有简单方法。

如果崩溃发生之后创建了一个内存转储文件,调试这个文件与在调试器中调试时发生崩溃的情况是相似的。下面的节显示了一个现场调试的例子,但它与分析一个转储文件相似。

调试一个Bug

这里分析Bug的检查。例子中,当崩溃发生时,内核调试器被附上,内核模式转储文件的分析过程都是相似的。

在这个例子中,例子驱动Sioctl.sys被加载,一个断点被设置在Sioctl!DriverEntry。当调试器停在这个断点的时候,设置Eip为0,这个值永远是非法的值,因为指令指针不能为0。然后按F5让它运行。一个内核错误将会发生,并且一个bug检查将会出现。你可以然后用!analyze扩展命令来检查:

kd> !analyze -v

*******************************************************************************

*                                                                             *

*                        Bugcheck Analysis                                    *

*                                                                             *

*******************************************************************************

 

SYSTEM_THREAD_EXCEPTION_NOT_HANDLED (7e)

This is a very common bugcheck.  Usually the exception address pinpoints

the driver/function that caused the problem.  Always note this address

as well as the link date of the driver/image that contains this address.

Arguments:

Arg1: c0000005, The exception code that was not handled

Arg2: 00000000, The address that the exception occurred at

Arg3: f88f2bd8, Exception Record Address

Arg4: f88f2828, Context Record Address

 

Debugging Details:

------------------

 

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - The instruction at "0xlx" referenced memory at "0xlx". The memory could not be "%s".

 

FAULTING_IP:

+0

00000000 ??               ???

 

EXCEPTION_RECORD:  f88f2bd8 -- (.exr fffffffff88f2bd8)

ExceptionAddress: 00000000

   ExceptionCode: c0000005 (Access violation)

  ExceptionFlags: 00000000

NumberParameters: 2

   Parameter[0]: 00000000

   Parameter[1]: 00000000

Attempt to read from address 00000000

 

CONTEXT:  f88f2828 -- (.cxr fffffffff88f2828)

eax=ffff99ea ebx=00000000 ecx=0000bb40 edx=8055f7a4 esi=e190049e edi=81e826e8

eip=00000000 esp=f88f2ca0 ebp=f88f2cf0 iopl=0         nv up ei pl nz na pe nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010202

00000000 ??               ???

Resetting default scope

 

DEFAULT_BUCKET_ID:  DRIVER_FAULT

 

CURRENT_IRQL:  0

 

ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at "0xlx" referenced memory at "0xlx". The memory could not be "%s".

 

READ_ADDRESS:  00000000

 

BUGCHECK_STR:  0x7E

 

LAST_CONTROL_TRANSFER:  from 805b9cbb to 00000000

 

STACK_TEXT: 

WARNING: Frame IP not in any known module. Following frames may be wrong.

f88f2c9c 805b9cbb 81e826e8 8123a000 00000000 0x0

f88f2d58 805b9ee5 80000234 8123a000 81e826e8 nt!IopLoadDriver+0x5e1

f88f2d80 804ec5c8 80000234 00000000 822aeda0 nt!IopLoadUnloadDriver+0x43

f88f2dac 805f1828 f7718cf4 00000000 00000000 nt!ExpWorkerThread+0xe9

f88f2ddc 8050058e 804ec50d 00000001 00000000 nt!PspSystemThreadStartup+0x2e

00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16

 

 

FAILED_INSTRUCTION_ADDRESS:

+0

00000000 ??               ???

 

FOLLOWUP_IP:

nt!IopLoadDriver+5e1

805b9cbb 3bc3             cmp     eax,ebx

 

SYMBOL_STACK_INDEX:  1

 

SYMBOL_NAME:  nt!IopLoadDriver+5e1

 

MODULE_NAME:  nt

 

IMAGE_NAME:  ntoskrnl.exe

 

DEBUG_FLR_IMAGE_TIMESTAMP:  3e800a79

 

STACK_COMMAND:  .cxr fffffffff88f2828 ; kb

 

FAILURE_BUCKET_ID:  0x7E_NULL_IP_nt!IopLoadDriver+5e1

 

BUCKET_ID:  0x7E_NULL_IP_nt!IopLoadDriver+5e1

kd> .cxr fffffffff88f2828

eax=ffff99ea ebx=00000000 ecx=0000bb40 edx=8055f7a4 esi=e190049e edi=81e826e8

eip=00000000 esp=f88f2ca0 ebp=f88f2cf0 iopl=0         nv up ei pl nz na pe nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010202

00000000 ??               ???

kd> kb

  *** Stack trace for last set context - .thread/.cxr resets it

ChildEBP RetAddr  Args to Child             

WARNING: Frame IP not in any known module. Following frames may be wrong.

f88f2c9c 805b9cbb 81e826e8 8123a000 00000000 0x0

f88f2d58 805b9ee5 80000234 8123a000 81e826e8 nt!IopLoadDriver+0x5e1

f88f2d80 804ec5c8 80000234 00000000 822aeda0 nt!IopLoadUnloadDriver+0x43

f88f2dac 805f1828 f7718cf4 00000000 00000000 nt!ExpWorkerThread+0xe9

f88f2ddc 8050058e 804ec50d 00000001 00000000 nt!PspSystemThreadStartup+0x2e

00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16

在堆栈中最上面的项看到了错误。这是在转储文件中可以遇到的。如果你不知道怎么检查已经发生的bug你怎么办呢?

1.    执行.frame 1,到这个神秘程序的调用者所在的帧,nt!IopLoadDriver。

2.    到反汇编窗,在那里nt!IopLoadDriver的调用能被显示出来,指令:

8062da9e ff572c           call    dword ptr [edi+0x2c]

8062daa1 3bc3             cmp     eax,ebx

3.    调用地址是一个双字指针,指向Edi寄存器的值加上0x2c。这就是你需要的地址,所以,显示一下Edi寄存器:

kd> r edi

Last set context:

edi=81a2bb18

4.    小小的算术:

kd> ? 81a2bb18+0x2c

Evaluate expression: -2120041660 = 81a2bb44

5.    哦,地址被保存在0x81A2BB44:

kd> dd 81a2bb44 l1

81a2bb44  f87941a3

6.    那个地址处有什么?

kd> dt f87941a3

GsDriverEntry

 SIoctl!GsDriverEntry+0(

         _DRIVER_OBJECT*,

         _UNICODE_STRING*)

于是,你就确定了堆栈顶端真正的程序。

伪寄存器

伪寄存器是可变的,你可以当做变量来用。很多伪寄存器被事先定义好用于特定的含义:$ra用于调用堆栈的当前项中的返回地址,$ip用于指令指针,$scopeip用于当前作用域(局部上下文,使得当前程序中局部变量可用)的地址,$proc当前EPROCESS的点等等。这些在条件语句中可能很有用。

也有一些伪寄存器定义了操作的含义,$t0到$t19可以用于各种目的,比如对断点计数。一个伪寄存器在现实情况中使用是更新存储块。

ba w4 81b404d8-18 "r$t0=@$t0+1;as /x ${/v:$$t0} @$t0;.block {.echo hit # $$t0};ad ${/v:$$t0};dd 81b404d8-18 l1;k;!thread -1 0;!process -1 0"

上面的大概意思是,当0x81B404D8处的双字被更新的时候,更新伪寄存器$t0作为一个命中计数器,告诉我们命中的次数并显示该值在0x81B404D8处,这里是当前进程的当前线程的调用堆栈(要理解上面的细节,参见下面的“别名aliasing”)。

另一个用法是常遇到的情况,需要跟踪Atapi.sys的DPC程序的活动(Atapi是一个标准的操作系统驱动程序)。这个程序频繁被调用,而查看的工程师知道在特定的点上有感兴趣的irp已经完成,变量irp指向那个IRP。在Tape.sys中的另一个例程(另一个标准的操作系统驱动程序)有一个名字叫做Irp的变量,它指向同一个IRP。工程师想在恰当的时机断住Tape.sys,所以他在Atapi.sys的DPC上设置了一个一次性断点,然后开始运行:

bp /1 Atapi!IdeProcessCompletedRequest+0x3bd "dv irp; r$t0=@@(irp)"

那个断点的动作设置成给伪寄存器$t0赋值为变量irp的值,也就是感兴趣的IRP的地址(显示irp的值)。

当一次性断点被命中的时候,工程师这么做:

bp Tape!TapeIoCompleteAssociated+0x1c6 "j (@@(Irp)=$t0) '.echo stopped at TAPE!TapeIoCompleteAssociated+0x1c6; dv Irp' ; 'g'"

这个意思是:当Tape.sys中两个断点被命中时,如果局部变量Irp等于$t0,输出一些引人注目的信息,并显示出Irp的值。另一方面,如果Irp不等于$t0,就继续运行。当这两个断点导致停止执行的时候,控制被挂起在了工程师想停止的地方。

别名

在命令串中用一个字符串替代另一个字符串可能是比较方便的,一个用法是定义一个比较短的字符串来代替一个比较长的命令,例如:

kd> as Demo r; !process -1 0; k; !thread -1 0

kd> al

  Alias            Value 

 -------          -------

 Demo             r; !process -1 0; k; !thread -1 0

kd> demo

Couldn't resolve error at 'emo'

kd> Demo

eax=00000001 ebx=001a6987 ecx=80571780 edx=ffd11118 esi=0000003e edi=f8bcc776

eip=804df1c0 esp=8056f564 ebp=8056f574 iopl=0         nv up ei pl nz na pe nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000202

nt!RtlpBreakWithStatusInstruction:

804df1c0 cc               int     3

PROCESS 80579f60  SessionId: none  Cid: 0000    Peb: 00000000  ParentCid: 0000

    DirBase: 00039000  ObjectTable: e1000e78  HandleCount: 234.

    Image: Idle

 

ChildEBP RetAddr 

8056f560 804e8682 nt!RtlpBreakWithStatusInstruction

8056f560 804e61ce nt!KeUpdateSystemTime+0x132

80579f60 00000000 nt!KiIdleLoop+0xe

THREAD 80579d00  Cid 0000.0000  Teb: 00000000 Win32Thread: 00000000 RUNNING on processor 0

注意,别名和它替代的字符串是大小写敏感的。

如果你回顾一下pseudo-registers,你就会看到第一次使用别名命令的例子,假如这是一个实际的命令:

bp SioctlDeviceControl "r$t0=@$t0+1;as /x ${/v:$$t0} @$t0;.block {.echo hit # $$t0};ad ${/v:$$t0};k ;!thread -1 0;!process -1 0;g"

 

这就是WinDbg命令窗的输出,红色部分是别名的显示:

hit # 0x1

ChildEBP RetAddr 

f747dc20 80a2675c SIoctl!SioctlDeviceControl

f747dc3c 80c70bed nt!IofCallDriver+0x62

f747dc54 80c71b0d nt!IopSynchronousServiceTail+0x159

f747dcf4 80c673aa nt!IopXxxControlFile+0x665

f747dd28 80afbbf2 nt!NtDeviceIoControlFile+0x28

f747dd28 7ffe0304 nt!_KiSystemService+0x13f

0006fdc8 04003bcb SharedUserData!SystemCallStub+0x4

0006fde8 04002314 ioctlapp!_ftbuf+0x1b

0006ff78 04002e02 ioctlapp!main+0x1e4

0006ffc0 77e4f38c ioctlapp!mainCRTStartup+0x14d

WARNING: Frame IP not in any known module. Following frames may be wrong.

0006fff0 00000000 0x77e4f38c

THREAD feca2b88  Cid 0714.0e2c  Teb: 7ffde000 Win32Thread: 00000000 RUNNING on processor 0

PROCESS ff877b50  SessionId: 1  Cid: 0714    Peb: 7ffdf000  ParentCid: 0d04

    DirBase: 048f0000  ObjectTable: e2342440  HandleCount:  19.

    Image: ioctlapp.exe

...

hit # 0x2

...

hit # 0x3

...

上面的基本技巧是定义命令块,使其与一个断点关联,以这样的方式嵌入别名的定义,不会立即进行估值而仅仅在触发断点的时候才开始估值。通过完美的使用${} (“别名解释 Alias Interpreter”)代表/v选项来指定一个别名不会在说明的时候进行估值(也就是bp命令的时候),.block (“Block”)使得别名在断点触发的时候估值并执行关联的命令。最后,as命令的/x选项确保使用64位值,ad确保最近的别名被清除。

用脚本文件或其它方式减少工作量

可以运行一个脚本来执行一些WinDbg命令。只是一个x64系统的转储文件的例子,重点关注的是一个驱动调用xStor.sys的SCSI请求块:

  1. 使用!irpfind (参见windows调试工具帮助文档)查找非分页内存池中的IRP,你可以得到这样的行:

fffffadfe5df1010 [fffffadfe5ee6760] irpStack: ( 4, 0)  fffffadfe78cc060 [ \Driver\dmio] 0xfffffadfe6919470

红色的值是那些特定的IRP的地址。

  1. 复制那些行到一个文件。
  2. 在文件中,选择包含xStor的所有的行,放到另一个叫做debugtst1.txt的文件中,就像下面这样:

fffffadfe5e4c9d0 [00000000] irpStack: ( f, 0)  fffffadfe783d050 [ \Driver\xStor] 0xfffffadfe6919470

  1. 编辑debugtst1.txt,修改每一行:

!irp fffffadfe5e4c9d0 1

!irp 扩展显示给定地址的IRP,包括IRP主要的域和它的堆栈,保存debugtst1.txt。

  1. 现在,在WinDbg中使用命令$$<c:\temp\debugtst1.txt,你会得到很多输出,开头是这样的:

1: kd> $$<c:\temp\debugtst1.txt

1: kd> !irp fffffadfe5e4c9d0 1

Irp is active with 2 stacks 2 is current (= 0xfffffadfe5e4cae8)

 Mdl = fffffadfe600f5e0 Thread 00000000:  Irp stack trace.

Flags = 00000000

ThreadListEntry.Flink = fffffadfe5e4c9f0

ThreadListEntry.Blink = fffffadfe5e4c9f0

IoStatus.Status = c00000bb

IoStatus.Information = 00000000

 

...

 

Tail.Apc = 0326cc00

Tail.CompletionKey = 0326cc00

     cmd  flg cl Device   File     Completion-Context

 [  0, 0]   0  0 00000000 00000000 00000000-00000000

 

                        Args: 00000000 00000000 00000000 00000000

>[  f, 0]   0 e1 fffffadfe783d050 00000000 fffffadfe3ee46d0-fffffadfe6869010 Success Error Cancel pending

               \Driver\xStor   CLASSPNP!TransferPktComplete

                        Args: fffffadfe6869130 00000000 00000000 00000000

这些条件下红色的值就是IRP的SRB地址。

  1. 为了获得SRB的格式,创建debugtst2.txt,查找并复制所有上面输出的、包含 “Args: ffff” 的行,然后像这样修改每一行:

dt nt!SCSI_REQUEST_BLOCK fffffadfe6869130

注意,因为微软符号库只包含“公共符号”,所以nt!SCSI_REQUEST_BLOCK可能不可用,为了眼下的目的,简单地在那个你打算有完整符号的驱动中定义这个结构就行了。

  1. 你先要保存debugtst2.txt,然后再WinDbg中你输入$$<c:\temp\debugtst2.txt,输出应该像这样:

1: kd> dt nt!SCSI_REQUEST_BLOCK fffffadfe6869130

   +0x000 Length           : 0x58

   +0x002 Function         : 0 ''

   +0x003 SrbStatus        : 0 ''

   +0x004 ScsiStatus       : 0 ''

   +0x005 PathId           : 0 ''

   +0x006 TargetId         : 0xff ''

   +0x007 Lun              : 0 ''

 

...

 

   +0x038 SrbExtension     : (null)

   +0x040 InternalStatus   : 0x21044d0

   +0x040 QueueSortKey     : 0x21044d0

   +0x044 Reserved         : 0

   +0x048 Cdb              : [16]  "*"

因此,经过几分钟的工作,你应该已经找到并显示出了所有有趣的SRB了。写一个调试器扩展来做同样的事情是可能的,但是对于一次性的探究,一个简单的脚本可能是更好的途径。你可以制作一个包含带有控制逻辑的命令脚本并做成命令程序,以执行更多东西。控制通过类似.if.for这样的流水作业来执行,更多关于脚本和命令程序的细节参见windows调试工具帮助文档中的“运行脚本文件 Run Script File”和“使用调试器命令程序 Using Debugger Command Programs ”。

一个调试器扩展更强大些,但是消耗的时间更多。一个扩展是用c或c++写的,被构建成一个动态库,使调试器和它的引擎更加强大。很多类似于!process这样通常使用的命令事实上是扩展。写一个扩展的细节不在本文介绍的范围,可以参见windows调试工具帮助文档的“调试器扩展Debugger Extensions”部分。

远程调试

WinDbg(以及KD)可以连接到一台作为服务器的目标机和一台作为客户机的调试器实例,经由TCP/IP或其它协议。测试系统经由COMx或1394连接调试器,调试器作为服务器,然后开发者可以在远处检查或运行。允许一个人在他/她自己的办公室检查实验室的问题这对自动化测试是非常宝贵。

为了得到这种能力,你可以用命令行启动调试器,用选项标识规则:

windbg -server tcp:port=5555

或者你也可以在WinDbg启动之后这样:

.server tcp:port=5005

两种方法都可以使WinDbg成为调试服务器,在端口5005侦听TCP/IP包。另一个不同的WinDbg实例中连接客户端:

.tcp:server=myserver,port=5005

从头开始启动WinDbg客户端:

windbg -remote tcp:server=myserver,port=5005

COMx、命名管道和SSL是其它可用的协议。

关于远程调试的几个认识:

  • 如果有一个防火墙在本地或公司网中的主机系统与另一个网络中的目标机系统之间,远程调试还更复杂些,参见windows调试工具帮助文档获取更详细的内容。
  • 访问符号库和源代码是基于一个人登录的远程服务器的权限的,而不是这个人正在运行的客户端的权限。
  • 客户机设置源文件位置使用.lsrcpath(“local source path”),而不是.srcpath。

 “短”调用堆栈

WinDbg尽量搞清楚调用堆栈,但是有时候却不能。对于调试者来说,接受这样一个现实是非常困难的,因为她或他必须使用他/她自己的知识来弥补WinDbg的不足。这里提醒你,艰苦的工作在后面呢。

考虑这个来自转储文件的例子,这里有一个double-fault 的bug检查

kd> k

ChildEBP RetAddr 

be80cff8 00000000 hal!HalpClockInterruptPn+0x84

好像只有一个线程在堆栈里,操作系统时钟中断程序。这有点可疑,应该是出错了。所以查查当前线程:

kd> !thread

THREAD 88f108a0  Cid 8.54c  Teb: 00000000  Win32Thread: 00000000 RUNNING

 

...

 

Start Address rpcxdr!SunRpcGetRxStats (0xf7352702)

Stack Init be810000 Current be80fd34 Base be810000 Limit be80d000 Call 0

堆栈在0xBE810000,延续到了0xBE80D000(3个页,这是正常的)。出错的时钟程序的堆栈基地址在0xBE80CFF8,这个地址超出了堆栈的末端。好像是时钟程序使用了超出标准堆栈的大小?

开始查找堆栈地址,该堆栈可能标识了另一个程序。做这个的有用工具是dds(“Display Words and Symbols”),用于查找存储地址(dqs和dps也能用,注意所有的这三个命令都是大小写敏感的)。现在忽略时钟中断程序,集中关注那个堆栈包括的、被时钟程序中断的程序。不是完全忽略时钟程序:你启动时它的堆栈基地址指针是在0xBE80CFF8。

接下来查看0xBE80CFF8,看看是否有更有趣的东西显示(下面的注释是用c风格的注释):

2: kd> dds be80cff8 be80cff8+0x100

be80cff8  ????????          

be80cffc  ????????          

be80d000  00000000

 

...

 

be80d034  00000000

be80d038  00000020

 

...

 

be80d058  be80d084

be80d05c  00000000

be80d060  bfee03e0 zzznds+0x103e0

be80d064  00000008

be80d068  00000246

be80d06c  8a61c004

be80d070  bfbb7858

be80d074  88c1da28

be80d078  00000000

be80d07c  00000000

be80d080  00000000

be80d084  be80d0d8          

be80d088  bfedbed7 zzznds+0xbed7

 

...

 

假定标识为“zzzndx+0x103E0”的行使驱动程序被时钟程序中断的地方。你应该注意更早的行(堆栈地址更高)被标识为“zzznds+0xBED7”。

现在看看zzznds+0xBED7之前的一小段反汇编指令:

zzznds+0xbed0:

bfedbed0 53               push    ebx

bfedbed1 57               push    edi

bfedbed2 e8e7420000       call    zzznds+0x101be (bfee01be)

注意,调用是在zzznds+0x101BE,与第一个标识行很近,反汇编正是调用到那里的。

看看反汇编在zzznds+0x101BE是怎么调用的:

bfee01be 55               push    ebp          

bfee01bf 8bec             mov     ebp,esp     

bfee01c1 83ec0c           sub     esp,0xc     

bfee01c4 53               push    ebx

回到dds的输出你可以看到,在0xBE80D088调用者保存了Ebp,压入Ebp这个操作(在0xBFEE01BE压Ebp)之后Esp是0xBE80D084,Bsp变成了当前Bep(0xBFEE01BF处的指令),接着在0xBFEE01C1处把Esp减去了oxc,结果Esp的值在0XBFEE01C4处是0xBE80D078。

现在你已经确定了zzznds+0xBED7调用的地方的Ebp、Esp和Eip的值,也就是0xBE80D084、 0xBE80D078 和 0xBFEE01C4,因此把它们作为k命令的参数:

2: kd> k = 0xBE80D084 0xBE80D078 0xBFEE01C4

ChildEBP RetAddr 

WARNING: Stack unwind information not available. Following frames may be wrong.

be80d084 bfedbed7 zzznds+0x101c4

be80d0d8 bff6030f zzznds+0xbed7

be80d0fc 8046d778 SCSIPORT!SpStartIoSynchronized+0x139

be80d114 bff60e4f nt!KeSynchronizeExecution+0x28

be80d148 8006627b SCSIPORT!SpBuildScatterGather+0x249

be80d174 8041d30e hal!HalAllocateAdapterChannel+0x11b

be80d18c bff5f8c8 nt!IoAllocateAdapterChannel+0x28

be80d1bc 8041f73f SCSIPORT!ScsiPortStartIo+0x2ea

be80d1e0 bff5f4ec nt!IoStartPacket+0x6f

be80d214 bff601d0 SCSIPORT!ScsiPortFdoDispatch+0x26c

be80d22c bff622f7 SCSIPORT!SpDispatchRequest+0x70

be80d248 bff5e390 SCSIPORT!ScsiPortPdoScsi+0xef

be80d258 8041deb1 SCSIPORT!ScsiPortGlobalDispatch+0x1a

 

...

 

这只是堆栈最后的一部分(还有更多),你应该已经有了大致的想法。

上面直接给k提供参数,对很多侦测工作是必要的,包括搜索堆栈和查看代码,看看堆栈在某个点上是怎么构建的。一种情况到另一种情况是不同的,这里的教程告诉你,如果WinDbg的堆栈看起来比较“短”,就看看内核分配给失败线程的堆栈是否是真正的原因。如果不是,就继续挖掘。

 

单步调试期间线程上下文的意外改变

如果你在内核模式单步调试期间消耗过多的时间(例如,按F10或F11),你可能会被告知控制被意外地跳转到了一个你不期望的点,如果你运行的IRQL小于DISPATCH_LEVEL时使用单步调试就会发生这样的事情。如果你知道当前运行在哪个线程,检查运行的线程,你将明白某个线程被改变了。

你感到困惑是正常的,调试器是通过在下一个指令处或下一个语句处放了一个调试指令(比如,x86架构的int 3),调试指令对调试着通常是不可见的。如果这个线程的在从当前指令移动到下一个指令的时间间隔过长导致时间片用尽,操作系统可能会调动另一个线程,那个线程就可能会遇到调试指令,于是,调试器获得控制权。调试器并不检查单步调试的上一个点和当前点是不是同一个线程,而是简单地停止执行在这个点上,在这种情况下,你可能看到了一个跳转。就像是如果你单步走过一行代码进行了很多处理,可能会发生单步走过一个api,而这个api又调用了别的api等等。

没有简单的方法来处理这种可以预期的行为。单步调试中要密切注意这种不希望的跳转,并在你怀疑这种情况发生时检查当前线程。如果你发现线程已经被切换了,你应该回头找到最后的一个好点,从那里重新启动调试,在那个好点上设置一个一次性的线程断点,直接运行到断点处,然后你再继续:仍然有被切换的风险,但是你可以走得更远些。

获取更多信息

本文不能贯穿该调试器的所有能力。要获取更多的信息,查阅windows调试工具帮助文档。如果你仍有不明白的问题,还可以在microsoft.public.windbg公共新闻组(msnews.microsoft.com主办)提问或发送电子邮件到windbgfb@microsoft.com。

 

转载自 http://blog.sina.com.cn/s/blog_6ea022670100n79r.html , 命若琴弦.

posted @ 2015-05-24 14:16  Kevin77  阅读(1110)  评论(0)    收藏  举报