使用WinDbg内核调试
看雪学院,笨笨雄译
安装程序
基础
挑选技术
取得更多信息
WINDOWS调试工具很强大,但是学习使用它们并不容易。特别对于驱动开发者使用的WinDbg和KD这两个内核调试器(CDB和NTSD是用户态调试器)。本教程的目标是给予一个已经有其他调试工具使用经验的开发者足够信息,使其能通过参考WINDOWS调试工具的帮助文件进行内核调试。本文将假定开发者熟悉一般 WINDOWS操作系统和进程的建立过程。
本文的重点是集成内核模式和用户态模式的图形化调试器WinDbg。KD在脚本和自动化调试中更有用,并且在资深程序员中拥有一定地位,但是本教程将集中讨论WinDbg,只会偶尔提到KD。
本文讨论的是Windows NT 4.0,Windows 2000或以后的版本,而且目标电脑的处理器基于X86架构。对于64位平台,将不会特别提及。
总之,本教程由简单介绍调试器的安装开始,大体分成2部分,基础知识和选择技术。基础知识包括基本调试命令和常用调试命令。选择技术是其他命令和在很多情况下都有用的调查方法。后者并不是调查象deadlocks, memory corruption或者resource leaks的唯一方法。第一次阅读本教程,你可能会跳过选择技术。你可以停止阅读本教程而转向微软调试器讨论组,也可以通过调试器的反馈e-mai解决更多的问题。
安装程序
取得最新版!
取得最新版的调试器,并且有规律的更新它。这里并没有夸大最新版的价值,因为调试器会经常改进和修复错误。你将能在下面网址下载:
http://www.microsoft.com/whdc/devtools/debugging/default.mspx .
主机与目标之间的连接
调试器有使用null-modem cable 或者1394 cable连接两台电脑的安装方案 。本教程不分析单操作系统的本地调试(即在调试器运行的电脑上进行分析)。3台电脑(目标电脑,调试服务器,调试客户端)的调试将会被简要的讨论。
在主机调试软件(WinDbg或者KD)和目标操作系统之间,是一个协同处理的调试过程。每一部分都必须做些什么。更明确地,WinDbg 不是作为一个"管理操作系统",象客户和一个真正操作系统那样运行目标。WinDbg是一个调试软件,象目标操作系统的合作伙伴那样知道它在调试过程中的角色。在这种关系中,WinDbg从目标接收信息,并且向目标发送信息。这是一种有效的通信机制。
serial protocol是调试器与目标系统之间可靠的通信机制。你能通过null-modem cable使用COM端口连接主机和目标机器。另一个可供选择的通信机制是1394。在调试工具的帮助文件中的"Configuring Software on the Target Computer." 主题有关于它们的描述。
你的第一次session
假设你的主机使用WIN2K或以上的版本。主机的操作系统可以不同于目标电脑的操作系统。主机可以在你平常进行开发,维护或者故障诊断的地方。它应该与网络连接,如果你希望访问symbol和source服务器(请看symbols和source)。
从命令提示窗口中,改变当前的目录到WINDOWS调试工具的安装目录。这是windbg.exe 和kd.exe 所在的位置。输入windbg,按下Enter。你将会看到:
分屏
在这里,你能重排你的窗口。下面的例子包括可移动的窗口。打开组合窗口并移到屏幕上方,单击"Command"标题栏并拖动它的窗口离开主框架。然后收缩主框架,你可以使用键击代替直接使用菜单或者按钮。
然后使用File Kernel Debug 以得到一个协议窗口,选择1394和channel 1。到这里,你的桌面会象下图一样:
在Kernel Debugging窗口中,点OK。
激活连接
现在你已经准备好在主机和目标之间建立连接。在目标机器以其中一个调试入口启动WINDOWS。立即回到主机系统,用鼠标激活WinDbg 的命令窗口,按下CTRL+BREAK 。不久之后,你会看到:
现在不必担心关于symbols的信息。你已经将WinDbg连接到WIN 2003。你现在很忙!
你需要明白一件细小却至关重要的事:在命令窗口的底部显示"kd>"提示符。这代表WinDbg 已经准别好接受命令。如果没有提示符显示,这时WinDbg将不能处理命令,尽管你输入的任何命令都将会被保存在缓冲区域并尽可能快的运行。你必须等待"kd>"出现,以确定WinDbg已经作好响应的准备。因为有时它正在忙于做某些你看不见的事(例如从目标取得信息,该信息可能很庞大)。缺少"kd>"是WinDbg处于繁忙状态的唯一线索。另一个可能是WinDbg试图解析symbol并且时间超过了你的预期。不幸地,WinDbg 偶尔会等待一个永远不会响应的目标连接(可能boot.ini配置得不好,或者选择了错误的选项)。在等待足够时间之后,你必须决定采取激烈的措施例如按下 CTRL+BREAK,或者停止WinDbg重新开始。
查找symbols和source
现在你很可能渴望开始调试,但仍然有一些东西你必须去做,因为它们将会很好的改善你的调试体验。
首先确认WinDbg能找到你感兴趣模块的symbols。Symbols指出一个二进制命令与声明之间的联系和什么变量正在被转移。换句话说,就是 Symbols表。如果你在建立模块的地方,那么你将拥有有效的symbols和source文件。但是如果你需要单步调试其他很早以前建立代码呢?或者,在那种情况下,如果你的代码不在它被建立的地方呢?
明确的设置symbols所在的地方,使用.sympath命令。在命令窗口中中断(CTRL-BREAK)然后输入:
.sympath SRV*<DownstreamStore>*http://msdl.microsoft.com/download/symbols
以便告诉WinDbg在Microsoft公开的symbols服务器上查找symbols。让WinDbg使用该服务以及在本地保存一份已下载的symbols。例如,在D:/DebugSymbols,你应该这么做:
.sympath SRV*d:/DebugSymbols*http://msdl.microsoft.com/download/symbols
你偶尔会在symbols服务器上获取symbols时遇到一些故障。在这个情况下,使用!sym noisy 命令以获得关于WinDbg尝试获取symbols的更多信息。然后使用 !lmi 查看WinDbg知道多少关于ntoskrnl的信息。然后尝试取得ntoskrnl的symbols,使用.reload /f。因而:
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调试工具帮助文件中,有关于这里使用的命令及其语法的描述。
输出symbols通常很大。WINDOWS调试工具包括一个symbol服务器,以便连接到Microsoft的网络服务器保存这些公开的symbol。添加这些到你的symbol路径,然后加载它们:
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
symbols 只会给你一些信息,而不会提供源代码。在最简单的情况下,在它们被建立的时候,source文件便在同一个地方(该位置包括2进制文件和symbol文件)。但是在大多数情况下,你不能在那里找到它们(它们可能被移走了),你必须指定在哪里能找到它们。这时,你需要一个源路径,例如,
.srcpath e:/Win2003SP1
它的意思是:想要source文件,请查看e:/Win2003SP1目录。
另一个解决方案是命名一个source服务器,如果你有:
.srcpath //MySrcServer
如果你曾经在获取source文件时遇到麻烦,使用.srcnoisy 1以取得更多关于调试器查找它们的信息。
Workspaces
目前你还不能开始调试,除非你已经准备好打很多字。很多设置都被保存在workspace中。所以你应该使用File Save 保存在workspace里面,例如,你将它保存为kernel1394Win2003。在这之后,你希望以这个workspace的设置启动 WinDbg:
windbg -W kernel1394Win2003 -k 1394:channel=1
-W指定一个workspace,而-k给出通信方式(祥见WINDOWS调试工具帮助文件中的"WinDbg Command-Line Options")。注意:在WinDbg或者KD中,你应该小心区分命令行可选项的大小写。
为了让事情变得简单,你可以在桌面建立快捷方式,以使用特定的workspace启动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,指定symbo路径(-y)和workspace (-W)。
一个示例驱动
使用示例驱动IoCtl练习,这将会帮助你熟悉WinDbg。你能在WINDDK和它的后续产品,WDK中找到。安装它,你便能在src/general /Ioctl子目录下找到该驱动。IoCtl的优点在于它是示例,而且是一个"legacy"驱动,由服务管理器(SCM)加载,而不是即插即用的一部分(这里并不关心PnP的输入和输出)。你应该建立用户态程序(ioctlapp.exe),并在前者被加载之后建立内核态驱动程序 (sioctl.sys)。
这里有些重要的事需要明白。在优化代码方面,建立程序的处理十分灵巧,优化会导致代码移动(当然,原逻辑会被保留),并且将一些变量单独保存在寄存器中。为了确保更简单的调试体验,你应该在建立窗口或者源代码文件中使用这些编译指令建立一个调试版本:
MSC_OPTIMIZATION=/Od
(这是"Oh d"而不是"zero d.")
有时上述的情况会引起内部函数的一些问题,例如memcmp。如果你碰上这个问题,尝试 :
MSC_OPTIMIZATION=/Odi
请明白阻止优化对于生成正式版产品来说,并不是一个好选择。使用上述的指令,你将不能建立或者测试正式版。尽管如此,这对于测试未经优化的版本来说,是不错的练习。一旦你熟悉代码,排除简单的错误,正式产品便能得到提升。如果你需要处理已优化的代码,你将会在"处理优化代码"找到相关帮助。
开始调试示例驱动
在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中将此路径设置为symbol路径和source路径。)在同样的命令窗口,输入 ioctlapp按下Enter,在WinDbg中,你会看到:
如图,程序停在断点之后,!lmi 命令显示WinDbg从DDK中取得symbols。时间信息象你期望的一样,本地symbol文件也符合你的要求。
依赖于你的排列方案,它并不明显,当前窗口能被其他窗口隐藏,但是你能在某个地方使用源代码窗口(按键顺序'alt-Keypad *' ― 不用按单引号― 将会把窗口置前):
断点被设置,即运行停止的地方会以粉红色标记(WINDOWS调试工具帮助文件把它称为紫色)。当运行进IoCreateDevice(运行控制 描述如何熟练运用):
这里你能看到原始断点(高亮为红色,现在控制将停止在这里),你能看到当前声明被标记为深蓝色。
基础
在调试session中,这是一个"测试驱动"。这是一些基本的调试操作。
命令,扩展,等等。
命令来自几个系列:简单的(未修饰的),一些从句号(".")开始,一些从惊叹号("!")开始。WINDOWS调试工具帮助文件将它们分别描述为 commands, meta-commands and extension commands。以现在的效果来看,这些系列非常接近。
断点
在运行中产生中断,是调试器的功能之一。这是一些实现方法。
" 在操作系统启动时中断
为了在操作系统启动时尽早中断,请确认WinDbg 已经连接,重新按CTRL-ALT-K直到你看到:
在下次启动时,在ntoskrnl加载之后的一小段时间,这时所有驱动还没有被加载,操作系统将会挂起,而WinDbg将会取得控制权。在系统引导时间,你可能会希望为驱动程序定义断点,这就是时机。
" 普通断点
最简单的设置断点的方法就是通过bp ("Breakpoint")命令。例如:
bp MyDriver!xyz
bp f89adeaa
第一行,这个断点设在模块中的一个名字(<module>!<name>);第二行,它被设置在一个给出的地址。当运行到其中一个断点时,操作系统就会挂起,并且把控制权交给WinDbg。(你可以在"寻找名字"看看如何为第二个命令取得地址。)
注意:第一个命令的语法假定操作系统已经加载该模块,以及在symbol文件或者外部名定义有足够可用信息关于识别xyz。如果不能在模块中找到xyz,调试器会这么告诉你这些。
" 延迟断点
说到驱动程序没有被加载,你最初的哪个断点,使用bu(见上述开始调试示例驱动)设置的是一个"可延迟的"断点。Bu命令的参数是一个模块及它里面的名字,例如:
bu sioctl!SioctlDeviceControl
SioctlDeviceControl 是一个入口点,或者其他在模块sioctl.sys中的名字。这个形式假定当模块被加载,足够有用的信息识别SioctlDeviceControl以便断点能够设置。(如果模块已经加载名字被找到,那么断点将会立即被设置)。如果操作系统找不到SioctlDeviceControl,调试器会提示,另外将不会在SioctlDeviceControl处挂起。
延迟断点的一个有用的特性便是它对modules!names操作。相比之下,一般断点对地址或者立即将modules!names解释为地址。延迟断点的另一个特性便是在引导的过程中会被记住(这不会影响明确地址的断点)。然而,延迟断点的另外一个特性使得即使关联模块被卸载,它仍然会被保留。相同情况下,一般断点将会被移除。
" 另外一个设置一般断点的方法是通过source窗口。返回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"是"enabled",而"d"是"disabled"。
" 假设你希望临时停止使用某个断点。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
" 相似的方法,永久移除断点号码,使用bc 1 ("Clear Breakpoint")。现在该断点将会从断点列表中消除。
" 然而,有时在操作系统或者驱动程序中,断点会被设置在一些频繁被激活的地方,你可能希望将它应用在一些环境或者条件操作,以便断点只在该情况下生效。这是基本格式:
bp SIoctl!SioctlDeviceControl+0x103 "j (@@(Irp)=0xffb5c4f8) ''; 'g'"
它的意思是:只有Irp=地址0xFFB5C4F8时才中断;如果条件不符合,继续运行。
更深入的探讨上述命令,并不是断点本身的状态。更准确的说,断点有一个操作项目(在双引号标记中);在该项目中,j ("Execute 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被检测(你能在寄存器中找到关于它们的处理方法),不会象你想的那样工作:
bp SIoctl!SioctlDeviceControl+0x103 "j (@eax=0xffb5c4f8) '.echo Here!' ; '.echo Skipping; g' "
原因是可能会将寄存器的值扩充到64位再计算,例如,扩充到0xFFFFFFFF`FFB5C4F8,这将不会与0x00000000`FFB5C4F8匹配。这导致只有32位的最高位为1和一些其他条件(例如,一个32位寄存器)才适用。在WINDOWS调试工具帮助文件中的"Sign Extension"有更详尽的资料(也可以看看"Setting a Conditional Breakpoint")。
断点可能包含一些条件式,附带或不附带条件操作。其中一个条件是激发"one-shot":断点只激活一次(激活之后便清除)。假如你只对第一次激活感兴趣,对于那些使用频繁的代码,这很便利。
bp /1 SIoctl!SioctlDeviceControl+0x103
另外一个有用的条件式测试一个进程或者线程:
bp /p 0x81234000 SIoctl!SioctlDeviceControl+0x103
bp /t 0xff234000 SIoctl!SioctlDeviceControl+0x103
它们分别代表,仅当进程块(EPROCESS)在0x81234000,才在指定的地方停止,以及仅当线程块(ETHREAD)在0xFF234000时才在指定地方停止。
该条件式能被组合为:
bp /1 /C 4 /p 0x81234000 SIoctl!SioctlDeviceControl+0x103
这代表,当call stack深度大于4(这里的大写C很重要,因为"c"代表"少于")和进程块在0x81234000时中断。
" 另外一种不同类型的断点,需要指定访问方式。例如:
ba w4 0xffb5c4f8+0x18+0x4
正如你所看到的,这个地址来自IRP,它的偏移0x18+0x4处即它的IoStatus.Information 成员。所以当某程序企图更新IRP中IoStatus.Information的这4个字节时,断点会被激活。这种断点被称为数据断点(因为它们由数据访问触发)或者处理器断点(因为它们由处理器执行,而不是调试器自己)。
表达式:MASM与C++
在驱动程序之中使用变量提供参数,如进程地址。你或许同意那是很容易的一件事。然而,你需要理解一些调试器的表达式。
调试器有两种评价表达式的方法,参考"MASM" (Microsoft Macro Assembler)和"C++"。引用WINDOWS调试工具帮助文件中的"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 命令以获得更多信息):
kd> dv Irp
Irp = 0xff70fbc0
该响应的意思是,Irp 变量包含0xFF70FBC0。更多地,dv 解释C++语法中的参数。该响应基于变量内容,而不是地址。你可以确认它:
kd> ?? Irp
struct _IRP * 0xff70fbc0
?? 总是以C++ 为基础(详见??命令)。假如使用MASM类型的赋值,尝试? (详见 ? 命令):
kd> ? Irp
Evaluate expression: -141181880 = f795bc48
这表示变量Irp 位于0XF795BC48。你可以通过使用dd (详见 dd 命令)显示内存数据,确认该变量真的包含数据0xFF70FBC0。
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 命令),Type和Size成员有一个似是而非的数据 :
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
有时,你会希望使用C++ 赋值代替MASM表达式。"@@" 前缀会完成它。扩展命令总是使用象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)将会请求WinDbg 格式化位于0xF795BC48的IRP stack。
你需要进一步了解的是:@@前缀相当通用,正如它的正式意思,使用不同于当前表达式中正在使用的赋值方法。如果大部分表达式是MASM,@@代表C++,如果它是C++,@@代表MASM。
最后一点建议:如果表达式不如你期望那样工作,考虑你是否在请求调试器理解MASM或者C++语法。
显示和设置内存,变量,寄存器等等
有一些方法可以显示和改变它们。
" 在当前例程中显示一个变量(当前的"scope"),使用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
上面的命令,按照你知道的来说,就是IRP在0xFF70FBC0,而事实上,这是在ntoskrnl映射出的IRP结构。
" 如果你对众多成员的区域中的一块感兴趣呢?取得成员的大小,例如:
kd> dt nt!_IRP Size 0xff70fbc0
unsigned short 0x94
更直接的方法是使用?? ("Evaluate C++ Expression") 命令:
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个命令各自的第二个参数是一个长度,由l (字母"l")后面的数值给出,例如0x10。)
第一个显示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
改变第一个字(2个字节),通过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 中没有这样的成员。第二,dt irp 是二义的,但是WinDbg显示了该实例,它的想法好象被修正了,其中一个在ioctlapp.exe而另外一个则在sioctl.sys。因为大小写是有意义的,你应该在任何时候都使用它。
关于ew的更多信息,有其他 "Enter Values"命令: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 (命令指针)为堆栈偏移为0x0指向的值,和Esp(堆栈指针)+0xC,有效的释放堆栈。WINDOWS调试工具帮助文件中的 "Register Syntax",解释了poi 命令和为什么寄存器一些地方需要加上"@"前缀。
你可能会问上述寄存器设置命令怎么用。考虑一下,当一个"坏"驱动的DriverEntry 将会引起故障检查("蓝屏")- 或许由于违规访问。你可以通过在ntoskrn加载时设置一个延迟断点处理这些问题。下面命令必须在同一行中:
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) 宣布已经进入DriverEntry 5) 继续运行。(当然,这技术仅仅移除DriverEntry 引起崩溃的可能性,例如违规访问。如果操作系统期待驱动程序供应函数,该函数将不可用,和可能是其他问题导致停机。)
在这里,你会想知道是否能用寄存器设置一个变量。例如,返回到IoCtl的dispatch routine:
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 命令带M参数,这里在命令和参数之间不允许空格):
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),并且改写你想要数值。例如,
如上图,你可以改写16进制的数值。
运行控制
在前面的部分(详见IoCreateDevice)你曾经想程序从一点运行到下一点,而不需要告诉它怎么做。这里有一些方法可以控制运行。下面所有的项目,除第一项,都假设程序处于挂起状态。
" 中断(CTRL-BREAK ) - 该快捷键总是中断系统,只要系统正在运行并与WinDbg 处于通信状态 (在KD 快捷键是CTRL-C)。
" 步过(F10) - 每按一次运行一条语句(如果C 或者C++ 和 WinDbg处于"source mode", 可通过Debug Source Mode切换),或者一条指,并且规定如果遇到一个函数调用,将会运行过该函数,而不会进入它。
" 步进(F11) - 就象步过那样,除了运行到一个函数调用时,会进入该调用例程。
" 步出 (SHIFT-F11) - 这会使程序运行直到完成当前例程(在call stack中的当前地址)。如果你对该例程已经了解得足够多,这个快捷键很有用。
" 运行到光标(F7 or CRTL-F10) - 当你想运行到该处中断,你可以将光标放到源代码窗口或者反汇编窗口中相应的位置,按下F7;程序将会运行到该位置。有一点要注意,然而:如果运行流程与该处不匹配(例如,一个IF语句不运行),WinDbg 将不会中断,因为并没有运行到指定地方。
" 运行 (F5) - 运行直到遇到断点或者错误事件被检测到。你可以将"运行"想象为正常执行状态。
" 将指令设置在当前行(CTRL-SHIFT-I) - 在源代码窗口,你可以把光标放在一行中,使用该快捷键,只要你允许(例如 F5或者F10),程序便从该处开始运行。在你想重复一些指令序列时,这很有用。但是要注意一些事情。例如,寄存器和变量的数据不会象你正常运行到该处时看到那样。
" 直接设置Eip - 你可以为Eip寄存器设置一个数值,然后按下F5(或者F10或者其他的什么),运行开始于该地址。显然易见,该功能就象将指令设置在当前行,除非你指定了一个汇编指令的地址。
call stack
几乎运行到某一点,都会有一个区域作为堆栈使用;该堆栈用于存放本地状态,参数和返回地址。在内核空间中有一个内核栈,在用户空间中有一个用户栈。当中断发生时,可能有几个例程在当前的栈中。例如,如果由于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
最高一行(最新的)栈帧就是停止的地方。你也可以看到此前的一些调用。但是如果你没有symbols,他们可能会显示得不正常。在驱动中的当前文件和行号信息,都会在每个栈帧中呈现,你将会享受到在sioctl.sys中使用symbols的乐趣。
你可以为IoCtl的IRP处理程序打开源代码窗口。但是假如你对更早的例程不感兴趣?你打开调用窗口 (View Call stack),所以:
你可以双击入口,然后便会被带到源代码文件中,如果该文件已被定位。
如果你只对在堆栈中属于例程的变量感兴趣,你可以双击该例程所在的项目,或者你可以用kn (与k同属) 然后.frame。例如,取得关于调用了PrintIrpInfo的dispatch routine的信息:
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")命令能定位模块中的symbols。例如,如果你想在Ioctl 例程中设置断点,以便处理DeviceIoControl IRPs 。但是你不太记得该例程的名字了,你可以这么做:
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."的symbols。
这看起来看琐细。然而,想想,在一个实际案例中,这个信息在调试器中出现:
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>
由这些信息可以得出,你应该在被显示为红色的例程中设置断点。
处理优化代码
如果一个EXE文件在建立时作了一些优化,它可能很难在源码窗口中跟踪运行,一些本地变量可能无法使用,或者显示为错误的数值。对于x86指令,你可能要尝试在源代码窗口和反汇编窗口(将这些窗口并排会方便你工作)中跟踪它的运行。你不需要为了跟踪控制流而对x86非常了解;主要看比较命令(例如test或者cmp)和分支命令(例如jnz),以便跟踪控制流。
挑选技术
那适用于基本操作。尽管上面的焦点不是讨论如何调查一些特殊区域,但是有大量调试器命令 - 从技术上来说,它们是扩展命令并且由DLL提供 - 仍然值得被提及,因为它们在很多方面都被反复使用。
进程和线程
查看当前进程(在停止的位置):
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 miniport driver 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
这里有4个设备对象。通常查看第一个,使用!devobj 取得一些关于该设备的信息,而!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"
IRPs
最普遍的与驱动程序的通信便是发送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
偶然会用到的命令!irql (Windows Server 2003或以后的版本可用),因为它显示有关处理器当前的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);
{ /* Begin added code */
KIRQL saveIrql;
ULONG i = 0;
KeRaiseIrql(DISPATCH_LEVEL, &saveIrql);
i++;
KeLowerIrql(saveIrql);
} /* End added code */
break;
现在,如果你在KeRaiseIrql 之后的语句设置断点,并且发生中断:
kd> !irql
Debugger saved IRQL for processor 0x0 -- 2 (DISPATCH_LEVEL)
顺便说明一下!pcr 命令一般不会显示你感兴趣的IRQL,也就是说在该IRQL的断点引起中断。
Dump 文件
这里有一些DUMP文件独有的事要说明。只有少数一些事值得说明。
" 有三种内核DUMP文件。全部内存的DUMP 是最好的,但是有些更小体积的内核DUMP已经可以满足大多数情况。也有小内存DUMP,它只有64KB(比起其他两种类型生成得更快)。由于小内存 DUMP没有关于执行体的所有信息,你可能需要使用.exepath 命令指定执行体镜象。你可以通过配置WINDOWS以便当崩溃出现时建立一个DUMP文件。
" 调查DUMP文件时,不需要为WinDbg指定目标系统。在WinDbg 中使用File Open Crash Dump 打开DUMP文件。如果symbol路径和source 路径都已经设置好,它们会帮助你。
" 现在,在WinDbg的命令窗口 使用 !analyze -v取得摘要。该命令可能会提出执行上下文 (.cxr);通过设置该上下文,你可以访问错误发生时的call stack (最接近错误的那个)。你需要进入进程和线程 (!process 和 !thread),查看内核的模块列表 (lmnt),在该列表中挑选需要查看的驱动对象 (!drvobj) 和可能要查看设备节点 (!devnode),设备对象(!devobj)和设备堆栈 (!devstack)。但是在查看DUMP文件中,没有比使用!analyze -v更简单的方法了。
如果一个内核模式的DUMP文件在错误发生时被建立。调试该文件与使用调试器附加调试错误时相似。下面的部分将会展示一个现场调试的例子,它与分析DUMP文件相似。
调试错误
这是关于如何开始分析一个错误。在这个例子中,内核调试器在崩溃时附加,它的过程与分析一个内核模式DUMP文件是相似的
在这个例子中,Sioctl.sys 被加载,并且在Sioctl!DriverEntry设置断点。当调试器在该断点停止时,甚至EIP为0。这永远都不会是一个有效的数值,因为命令指针不能为0。然后通过F5继续运行。一个内核错误发生,你可以开始查错了。然后你可以使用!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 "0x%08lx" referenced memory at "0x%08lx". 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 "0x%08lx" referenced memory at "0x%08lx". 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
最上层的堆栈入口看起来是错误的。这是你可能在DUMP文件中遇到的。如果你不知道该错误是如何发生的,你应该如何工作?
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 是当前call stack入口的返回地址,$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中的数值、当前的call stack ,当前的进程和当前的线程 (请参考下面的别名使用以获得更详细描述)
另外一个用途说明来自于一个维护实例。该实例需要跟踪Atapi.sys的DPC 例程的活动状况(Atapi.sys 是一个标准的操作系统驱动程序)。该例程经常会被使用,分析工程师对一个特殊的地方感兴趣,一个IRP将要完成,而变量irp 指向相同的IRP。该工程师希望在正确的时间停止Tape.sys,所以他在开始的时候为Atapi.sys DPC 设置了一个只中断1次的断点:
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
注意被替换成的假名是区分大小写的。
如果你回到上面看看伪寄存器,你便会理解这里的第一个例子。继续,假设这是一个实际的命令:
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 确保最近的假名被清除。
Script文件和其他减少工作量的方法
你可以使用script文件运行大量WinDbg 命令。想象这是一个64位系统中的DUMP文件。这里的焦点是来自于xStor.sys设备驱动的 SCSI Request Blocks (SRBs) 调用:
1. 使用!irpfind (详见WINDOWS调试工具帮助文件) 查找在non-paged pool中的IRPs。你会得到下面几行字:
fffffadfe5df1010 [fffffadfe5ee6760] irpStack: ( 4, 0) fffffadfe78cc060 [ /Driver/dmio] 0xfffffadfe6919470
被红色显示的地址便是该特殊的IRP。
2. 复制这几行到一个文件中。
3. 在该文件中,选择所有包括 xStor的项目并且将这些项目放到另外一个文件中,debugtst1.txt。该输出行是:
fffffadfe5e4c9d0 [00000000] irpStack: ( f, 0) fffffadfe783d050 [ /Driver/xStor] 0xfffffadfe6919470
4. 编辑debugtst1.txt,修改每一行:
!irp fffffadfe5e4c9d0 1
!irp 扩展命令在给出的地址中显示IRP,包括IRP的首部和它的堆栈。保存debugtst1.txt。
5. 现在,在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地址。
6. 为了得到 SRBs 的格式化输出,复制上面所有包括 'Args: ffff' 的输出并保存在debugtst2.txt。然后象这样改变每一行:
dt nt!SCSI_REQUEST_BLOCK fffffadfe6869130
注意:因为Microsoft symbol 只保存"公开的symbols",nt!SCSI_REQUEST_BLOCK可能无法使用。为了当前的目的,想象它已经被定义在驱动程序的完整 symbols。
7. 保存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] "*"
这样,通过几分钟的工作,你找到并显示所有你感兴趣的SRBs。你也可以写一个调试器插件完成同样的工作,但是对于一次性的调查,一个简易的script会是更好的方法。你可以更进一步通过逻辑控制封装一些命令以组成一些命令程序。由.if 和 .for控制流水作业。更多内容请见WINDOWS调试工具帮助文件中的 "Run Script File" 和 "Using Debugger Command Programs"。
调试器插件功能更加强大,但是你需要花费更多时间去编写。由C 或者C++ 编写,编译成DLL,可以使用调试器的所有功能和它的引擎。一些常用的命令,例如!process ,实际上,它是由插件提供的。编写插件的细节部分已经超出本文范围,请参考WINDOWS调试工具帮助中的 "Debugger Extensions"。
远程调试
WinDbg (和 KD) 能够连接目标以扮演服务器的角色,而调式实例则扮演客户的角色,通过TCP/IP或者其他协议。待测系统通过 COMx 或者1394连接到调试器,调试器则提供调试服务。然后开发者可以远距离调查程序或者运行函数。在自动化测试中,安装该调试器很有价值,它允许你在自己的桌面上研究实验室的难题。
你可以使用该命令行选项表明它的任务以取得该特性:
windbg -server tcp:port=5555
或者你可以在WinDbg运行之后使用下面命令:
.server tcp:port=5005
任何一个方法都可以让WinDbg 扮演调试服务器的角色,在TCP/IP的5005端口监听。
另一个不同的WinDbg实例,使用下面命令作为客户端连接:
.tcp:server=myserver,port=5005
临时启动一个WinDbg客户端:
windbg -remote tcp:server=myserver,port=5005
COMx, named-pipe 和SSL 是其他可用的协议。
关于远程调试的一些事:
" 如果本地系统网络和目标系统网络之间有防火墙,那么远程调试将更加复杂。详见WINDOWS调试工具帮助文件。
" 访问symbols和 source依赖于你在远程服务器中登陆的权限,而不是客户机使用者的权限。
" 客户通过.lsrcpath ("local source path") 命令定位source文件,而不是.srcpath.
"Short" call stacks
WinDbg尽它的最大努力计算出call stack,但是有时它失败了。检索出这样一个状况是调试人员所面对的最难任务,因为他或者她必须用自己的知识对WinDbg作补充。注意,然后坚持向前。
假设这个例子来自于一个双重错误的DUMP文件(来自第一个参数0x00000008的非预期的内核模式陷阱):
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页)。显然,失败的时钟例程的栈基 (ChildEBP)是0xBE80CFF8,在正常状态堆栈结束位置的上面。时钟例程会使用超过标准的堆栈?
现在的侦察工作是查看堆栈中可能指出其他例程的地址。一般使用dds ("Display Words and Symbols")寻找已保存的地址 (也可以使用dqs 和 dps ;注意到3个命令都是区分大小写的)。为了现在的目的忽略时间中断例程,将焦点移到堆栈上面。但是没有例程被时钟例程中断。但是不要完全回避时钟例程:从这个事实开始,它的栈基指针(ChildEBP上面)是0xBE80CFF8。
查看0xBE80CFF8,看看是否有什么有趣的东西显示出来 (下面以C风格给出注释):
2: kd> dds be80cff8 be80cff8+0x100
be80cff8 ???????? /* Invalid storage address. */
be80cffc ???????? /* Invalid storage address. */
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 /* Saved Ebp of zzznds+0xBED7, as explained below. */
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 /* Save the caller's EBP. */
bfee01bf 8bec mov ebp,esp /* Make the current ESP our EBP. */
bfee01c1 83ec0c sub esp,0xc /* Adjust ESP by subtracting 0xC. */
bfee01c4 53 push ebx
回到上面,看看dds 的输出,你可以看到调用程序被保存在0xBE80D084的Ebp。指令将Ebp压入(在0xBFEE01BE push ebp ) 并且在0xBE80D084 保存它。这代表ESP会在压栈之后变成0xBE80D084,然后ESP变成当前的EBP (0xBFEE01BF处的指令),后来Esp减去0xC得到0xBFEE01C1。0XBFEE01C4处指令的结果是 Esp =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),你会注意到控制器突然跳转到非预期的地方。这很可能是该代码运行在低于DISPATCH_LEVEL的IRQL并且你使用了步过 (F10)。如果你知道你正在跟踪特定线程,现在检查正在运行的线程,你确定该线程的变化。
这很正常。调试器将调试指令(例如x86里的int 3)放在下一个指令或者下一个语句(在调试过程中,这些调试指令一般不可见)完成单步。如果移动当前指令/语句到下一个时,线程的时间片期满,操作系统可能分派不同的线程,该线程可能遇到调试指令,于是调试器便取得控制。调试器不会检查当前线程是不是上一次单步时的那个,仅仅停止运行。在这个情况下,你可以观察跳转。这个方案可能更接近步过需要大量处理的代码,正如步过一个API,该API又调用API,该API又调用API,等等。
没有简单的方法可以对付这个预期的重复出现的行为。当你在单步中看到一个非预期跳转时应该怎么做?检查当前线程,假如你觉得它很可疑。如果你发现该线程被关掉,你只能回到最后一个好的位置,重新测试,在该处设置一次性断点,让程序运行,直到它到达断点。然后你可以继续下去:你仍然易手该情况影响,但是你沿着感兴趣的路径更进一步。
取得更多信息
该摘要不会探索调试器的所有可能性。请参考WINDOWS调试工具帮助文件以取得更多信息。如果你仍然无法得到答案,将它们放到microsoft.public.windbg (由msnews.microsoft.com建立),或者发送email到[email protected] 。