Kernel - Debugging - Tutorial - With Windbg
Kernel - Debugging - Tutorial - With Windbg
Kernel - Debugging - Tutorial - With Windbg
Getting Set Up
Fundamentals
Selected Techniques
Getting More Information
The debuggers in Debugging Tools for Windows are powerful, but they have a steep
learning curve. This is particularly true for WinDbg and KD, the kernel debuggers used by
driver developers (CDB and NTSD are user-space debuggers). The aim of this tutorial is to
give a developer experienced with other debuggers enough information to get launched into
kernel debugging and to use the Debugging Tools for Windows help file as a reference. The
developer is assumed to be familiar with the general concepts of the Windows operating
system and the build process.
The focus will be mainly on WinDbg, a kernel-mode and user-mode debugger with a
graphical interface. KD is more useful for scripts and automated debugging and enjoys the
reputation of being the tool of choice of the most serious programmers, but this tutorial will
focus on WinDbg and will merely allude to KD from time to time.
The target operating system is Windows 2000 or later. Much of what is described here
works for Windows NT 4.0, too. Furthermore, the target computer's processor uses x86
architecture. While much here will work for 64-bit targets, no specific attention will be given
them.
In overview, the tutorial begins with a brief description of setting up for debugging. The bulk
of the tutorial is two sections, fundamentals and selected techniques. Fundamentals are the
basic and most-often used debugger commands. Selected techniques are those other
commands and investigative approaches that will be useful in many situations. This latter
section is not, however, an exploration of substantive techniques that might be used to
investigate things like deadlocks, memory corruption or resource leaks. On first reading of
the tutorial, you may want to skip the selected techniques. The tutorial concludes by
pointing to the Microsoft debugger discussion group and the debugger feedback e-mail
address for further questions.
Getting set up
Obtain the latest debugger, and update it regularly. The value of having the latest release
can hardly be overstated, because the debugger enjoys frequent improvements and fixes.
The debugger can be downloaded at
http://www.microsoft.com/whdc/devtools/debugging/default.mspx.
A serial protocol is the tried-and-true mechanism for communication between the debugger
application and the target system. You connect the host and target machines with a null-
modem cable at a COM port at each end. An alternative communication mechanism is 1394.
The Debugging Tools for Windows help file describes each of these and how to configure the
target system in the topic “Configuring Software on the Target Computer.”
Your host computer is assumed to be running Windows 2000 or later. The host operating
system can be a different version of Windows than the target operating system. The host
computer may be where you do your usual development, maintenance or troubleshooting.
It should be connected to the network if you want access to symbol and source servers (see
symbols and source).
From a Command Prompt window, change the current directory to the installation directory
of Debugging Tools for Windows. This is the directory in which windbg.exe and kd.exe are
Windowing
At this point you can arrange your windows. The following example involves floating
windows. Start with the combined window that is the first above. Click on the bar labeled
“Command” and drag that bar and its window away from the main frame. Then shrink the
main frame, since you can use keystrokes instead of directly using the menu or the buttons.
Then use FileàKernel Debug to get the protocol popup, and choose 1394 with channel 1.
At this point, your desktop looks like this:
Now you are ready to make a connection between host and target. Go to the target
machine and boot Windows from one of the debugging entries. Immediately go back to the
host system, touch the WinDbg command window with the cursor to make it active, and
press CTRL+BREAK. In a few seconds you should see this:
Don't worry for now about the messages concerning symbols. You have a WinDbg
connection to a Windows Server 2003 system. You're in business!
By now you are probably eager to start debugging, but there are a few more things you
should do, since they will improve your debugging experience enormously.
The first thing is to ensure that WinDbg can find the symbols for a module of interest.
Symbols indicate to what statement a binary instruction corresponds and what are the
variables in force. Symbols map, in other words. You have available the symbols and the
source files for your own modules, if they are in the same place as they were at build time.
But what if you need to step through some other code that may have been built long before
now? Or, for that matter, what if your code isn’t in the same location where it was built?
To explicitly set a location for symbols, use the .sympath command. Break (CTRL-BREAK)
in the command window and type
.sympath
SRV*<DownstreamStore>*http://msdl.microsoft.com/download/symbols
to tell WinDbg to look for symbols on the Microsoft public symbols server. To get WinDbg to
use that server and to keep a copy of downloaded symbols in a local store, for example, in
D:\DebugSymbols, you would do:
.sympath SRV*d:\DebugSymbols*http://msdl.microsoft.com/download/symbols
Occasionally you may have trouble in getting symbols from the symbols server. In such a
case, begin with the !sym noisy command to get more information about what WinDbg is
trying to do to obtain symbols. Next, use !lmi to see what WinDbg knows about the one
essential Windows module, ntoskrnl. Then try to get symbols for ntoskrnl, using .reload /f.
Thus:
kd> !lmi nt
Module: ntoskrnl
Size: 4d8000
CheckSum: 3f6f03
Characteristics: 10e
The syntax of the commands used here is described in the Debugging Tools for Windows
help file.
Exported symbols are usually pretty meager. Debugging Tools for Windows includes a
symbol server that can connect to a public symbol store on Microsoft's internet site. Add
this to your symbol path, and then load symbols:
kd> .sympath
SRV*d:\DebugSymbols*http://msdl.microsoft.com/download/symbols
d:\DebugSymbols\ntoskrnl.pdb\EC9B7590D1BB47A6A6D5383538C2B31A1\ntoskrnl
.pdb
kd> !lmi nt
Module: ntoskrnl
Size: 4d8000
CheckSum: 3f6f03
Characteristics: 10e
d:\DebugSymbols\ntoskrnl.pdb\EC9B7590D1BB47A6A6D5383538C2B31A1\ntoskrnl
.pdb
Compiler: C - front end [13.10 bld 2179] - back end [13.10 bld
2190]
While symbols will give you some information, they do not provide source code. In the
simplest case, source files will be found in the same place they were at build time (the
location will be in the binary and symbol files). But in many cases, they cannot be found
there (they may have been moved), and you must specify where to look. For that you need
a source path, for example,
.srcpath e:\Win2003SP1
.srcpath \\MySrcServer
If you experience trouble in getting source files, you should do .srcnoisy 1 to get more
information about what the debugger is doing to find them.
Workspaces
You’ve not begun actual debugging, yet you have done a good deal of typing already. A lot
of the settings can be kept in a workspace. So you might use FileàSave to save the
settings in a workspace, perhaps one you name kernel1394Win2003. After that, you could
start WinDbg with that workspace:
where –W specifies a workspace and –k gives the communication protocol (refer to the
Debugging Tools for Windows help file under “WinDbg Command-Line Options”). Note: You
should be careful to preserve lower and upper case in the command-line options you give to
WinDbg or KD.
To make things easier, you can put a shortcut on your desktop to start WinDbg with the
desired workspace, for example, with a 1394 connection:
The first line makes the Debugging Tools for Windows installation directory the current
directory, to ensure that debugger modules will be found. The second line starts WinDbg,
specifying a symbol path (-y) and workspace (-W).
A sample driver
It will be helpful to exercise WinDbg with the sample driver IoCtl. This can be found in the
Windows Device Driver Kit (DDK) and its successor, the Windows Driver Kit (WDK). Install
this kit and look in the src\general\Ioctl subdirectory. The advantages of IoCtl are that it is
simple and that it is a “legacy” driver, that is, one loaded by the Service Control Manager
(SCM) and not a part of Plug-and-Play (the in’s and out’s of PnP are not of interest here).
Here is something fairly important to understand. The build process is quite smart about
optimizing code, and optimization can result in code movement (logic is of course preserved)
and in keeping variable values solely in registers. To ensure a straightforward debugging
experience, you should produce a checked build with this compiler directive given in the
build window or in the appropriate sources file:
MSC_OPTIMIZATION=/Od
Sometimes the above will result in a build problem with intrinsic functions like memcmp. If
you run into that problem, try:
MSC_OPTIMIZATION=/Odi
Please understand that preventing optimization is not a good choice for a production build.
With the above directive, you would not be creating and would not be testing a production-
type build. It is nonetheless a good practice to start testing with non-optimized builds and,
once you are familiar with the code and have eliminated the simpler errors, to advance to
production builds. When you have to deal with optimized code, you will find some assistance
in dealing with optimized code.
Set a breakpoint in IoCtl at DriverEntry. Before starting the driver, break into the WinDbg
command window and type this:
bu sioctl!DriverEntry
The bu (“Breakpoint Unresolved”) command defers the actual setting of the breakpoint until
the module is loaded; at that point, WinDbg will look in it for “DriverEntry.” Since there is
nothing more to do now, hit F5 (or you could type g, “Go”).
Next, copy ioctlapp.exe and sioctl.sys to a place on the target system such as
C:\Temp\IOCTL, log in with Administrator privileges on that system, and make
C:\Temp\IOCTL the current directory in a command window there. (You need not put this
In the above, after execution stopped at the breakpoint, the !lmi command showed that
WinDbg did pick up the symbols from a DDK build. The timestamp is about what you would
expect, and the location of the symbol file is exactly that expected.
Depending on your docking arrangement, it may not be obvious, since windows can be
hidden by other windows, but you will have a source code window somewhere (the key
sequence ‘alt-Keypad *’ ― without single quotation marks ― will bring that window to the
front):
Fundamentals
That was a “test drive” in a debugging session. Here are the debugger’s basic operations.
Commands come in several families: Plain (unadorned), ones beginning with a period (“.”)
and ones beginning with an exclamation point (“!”). The Debugging Tools for Windows help
file describes these families as commands, meta-commands and extension commands,
respectively. For present purposes, the families are pretty much the same.
Breakpoints
Causing an interruption in execution is one of the basic capabilities of the debugger. Here
are some ways that can be done.
To get a break at the earliest point in operating system startup, be sure WinDbg is
properly connected, and do the key sequence CTRL-ALT-K repeatedly until you see:
At the next boot, the operating system will halt very shortly after ntoskrnl has
started but before any drivers have been loaded, and WinDbg will get control. This is
when you may wish to define breakpoints for drivers to be started at boot time.
• Ordinary breakpoints
bp MyDriver!xyz
bp f89adeaa
Note: The syntax of the first command assumes that the operating system has
loaded the module and that enough information is available through a symbol file or
through external names to identify xyz. If xyz cannot be found in the module, the
debugger will say so.
• Deferred breakpoints.
Speaking of drivers that haven’t loaded, your very first breakpoint, set by bu (see
starting to debug the sample driver above), was a “deferred” breakpoint. The bu
command takes as a parameter the module and a name in it, for example:
bu sioctl!SioctlDeviceControl
• To see all the breakpoints you’ve defined, use the bl (“Breakpoint List”) command:
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
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
The meaning of this is: Break only if the pointer Irp equals the address 0xFFB5C4F8;
if that condition is not met, proceed.
To go deeper into the above, the breakpoint is not itself conditional. Rather, the
breakpoint has an action clause (in double quotation marks); in that clause, the
command j (“Execute IF/ELSE”) is a conditional action. j's function is to execute its
TRUE|FALSE clauses (in single quotation marks). In the above, the TRUE clause (first)
is empty, so that when the breakpoint fires and the TRUE condition is met, WinDbg
would do nothing more than the default of halting. If the FALSE clause (second) is
met, execution would proceed, by executing g. One action or the other would be
done, depending on the condition in force.
Something to note: The following breakpoint, where register Eax is tested (you will
find registers treated more thoroughly in registers), won’t work as you might expect:
The reason is that evaluation may “sign-extend” the register’s value to 64 bits, i.e.,
to 0xFFFFFFFF`FFB5C4F8, and that would not match 0x00000000`FFB5C4F8. This
will be a issue only if the highest bit of the 32-bit value is 1 and if some other
conditions (for example, a 32-bit register) apply. “Sign Extension” in the Debugging
Tools for Windows help file gives details (see also “Setting a Conditional Breakpoint”
there).
A breakpoint may have conditions, with or without conditional actions. One condition
is “one-shot” firing: The breakpoint is to fire only once (it is cleared after being hit).
This is handy for a very high-traffic piece of code where you are interested in the
first hit only.
bp /1 SIoctl!SioctlDeviceControl+0x103
bp /p 0x81234000 SIoctl!SioctlDeviceControl+0x103
bp /t 0xff234000 SIoctl!SioctlDeviceControl+0x103
which mean, respectively, Stop at the indicated point only if the process block
(EPROCESS) is at 0x81234000, and Stop at the indicated point only if the thread
block (ETHREAD) is at 0xFF234000.
bp /1 /C 4 /p 0x81234000 SIoctl!SioctlDeviceControl+0x103
The meaning here is, Break once but only when the call stack depth is greater than
four (here capitalization of “C” is significant, because “c” means “less than”) and the
process block is at 0x81234000.
• A different kind of breakpoint is one that is specified for access. For example,
ba w4 0xffb5c4f8+0x18+0x4
You will probably agree that it is handy to use a variable in a driver program to provide a
parameter value like process address. Doing that, however, demands that you understand
something of debugger expressions.
The debugger has two ways of evaluating expressions, referred to “MASM” (Microsoft Macro
Assembler) and “C++.” To quote the Debugging Tools for Windows help file under “MASM
Expressions vs. C++ Expressions”:
In a MASM expression, the numerical value of any symbol is its memory address. In
a C++ expression, the numerical value of a variable is its actual value, not its
address.
Reading and re-reading that section will repay your time well.
2. The default type can be changed by .expr (see the Debugging Tools for Windows
help file).
Even this summary is fairly involved, and you should refer to “Evaluating Expressions” in
the Debugging Tools for Windows help file for the details. For now, here are a few examples
to give a sense of how evaluation works.
Earlier you were stopped at Sioctl!SioctlDeviceControl+0x103, so use dv to look at a
variable known there (see the dv command for more information):
kd> dv Irp
Irp = 0xff70fbc0
The response means, The variable Irp contains 0xFF70FBC0. Further, dv is interpreting its
parameter in C++ terms, in that the response is based on the variable’s content and not on
its address. You can confirm that so:
since ?? always functions on the basis of C++ (see the ?? command). For the MASM type of
evaluation, try ? (see the ? command):
kd> ? Irp
which means, The variable Irp is located at 0XF795BC48. You can confirm that the variable
at that location truly does contain the value 0xFF70FBC0 by displaying memory via dd (see
the dd command):
kd> dd f795bc48 l1
f795bc48 ff70fbc0
kd> dd 0xff70fbc0
This indeed looks like an IRP, as dt shows (see the dt command), since the Type and Size
members have plausible values:
kd> dt Irp
0xff70fbc0
+0x000 Type : 6
Sometimes you will want to employ C++ evaluation inside a MASM expression. The “@@”
prefix achieves that. Since extensions always use parameters as MASM expressions, you
can see the effect of @@ when it is employed with the extension!irp (see IRPs):
\Driver\SIoctl
A further thing to understand: The @@ prefix is rather versatile, for its true meaning is, Use
the evaluation method other than that currently being used in the surrounding expression.
If the overall evaluation is MASM, @@ means C++, and if it is C++, @@ means MASM.
A final bit of advice: If you cannot get an expression to work as you expect, consider
whether you’re asking the debugger to understand it in MASM or C++ terms.
• To display the variables in the current routine (the current “scope”), use dv (“Display
Variables”). For example, if stopped at Sioctl!SioctlDeviceControl+0x103:
kd> dv
DeviceObject = 0x82361348
Irp = 0xff70fbc0
outBufLength = 0x64
irpSp = 0xff70fc30
ntStatus = 0
mdl = 0x00000000
inBufLength = 0x3c
datalen = 0x26
This is a list of parameter variables and local variables known at the breakpoint.
“Known” is an important qualifier. For example, if a variable is optimized into a
kd> dv outBufLength
outBufLength = 0x64
• Another useful command is dt (“Display Type”). For example, continuing to use the
breakpoint at Sioctl!SioctlDeviceControl+0x103:
kd> dt Irp
0xff70fbc0
+0x000 Type : 6
The above says that the variable Irp is at 0xF795BC48 and that it contains
0xFF70FBC0; since dt knows the variable to be an IRP pointer (“Type _IRP*”), the
area at 0xFF70FBC0 is formatted as an IRP.
0xff70fbc0
+0x000 Type : 6
+0x000 Status : 0
+0x004 Information : 0
It is possible to display some structures even when they aren’t in scope (assuming,
that is, that the memory in question has not been reused for some other purpose):
+0x000 Type : 6
The command above exploits your knowledge that there is an IRP at 0xFF70FBC0
and the fact that there is a mapping of the IRP structure in ntoskrnl.
• What if you’re interested in a single field of a structure with many member fields?
Take the member Size, for example:
kd> ?? Irp->Size
(Note: The second parameter in each of the three commands above is a length,
given by l (the letter “l”) immediately followed by a value such as 0x10.)
The first displays 16 doublewords (four bytes each, or 64 bytes total) as double
words. The second displays the same as words. The third, the same as bytes.
kd> outBufLength = 00
kd> ?? outBufLength = 0
kd> dt Irp
0xff70fbc0
+0x000 Type : 6
To change the first word (two bytes, that is) via ew (“Enter Values”):
kd> dt Irp
0xff70fbc0
+0x000 Type : 3
kd> ?? irp->Type = 3
short 3
kd> dt irp
ioctlapp!Irp
0xff70fbc0
+0x000 Type : 3
There are a couple of things to notice in the above. First, the case of a member of a
structure is significant, as shown by WinDbg’s claim that there was no “type”
member of Irp. Second, dt irp was ambiguous, but WinDbg helpfully displayed the
instances it thought were likely fits, one in ioctlapp.exe and one in sioctl.sys. Since
case can be significant, you should employ it whenever you know it.
In addition to ew, there are other “Enter Values” commands: eb for a byte, ed for a
doubleword, eq for quadword (8 bytes) and so forth. Refer to the Debugging Tools
for Windows help file under “Enter Values.”
• The Locals window may be easier to use for things like a structure with imbedded
pointers to structures:
• Registers (as well as segment pointers and flags) can be displayed and altered. For
example:
kd> r
Or just:
kd> r eax
eax=81478f68
Sometimes you will want to alter a register. Eax, for instance, is often used at exit
from a routine to carry the return value, so, just before exit from a routine:
r eax = 0xc0000001
r eip = poi(@esp)
which mean, respectively, Set Eip (the instruction pointer) to the value found at
offset 0x0 from the stack pointer, and Add 0xC to Esp (the stack pointer), effectively
unwinding the stack pointer. The Debugging Tools for Windows help file, under
“Register Syntax,” explains poi and why register names have to be prefixed with a
single “@” in some places.
You may be asking yourself how the above register-setting commands could be
useful. Consider the case where there is a “bad” driver whose DriverEntry will cause
a bug check ("blue screen") — due to an access violation, perhaps. You could deal
with the problem by setting a deferred breakpoint when ntoskrnl loads. The following
command must be typed on a single line:
The meaning is: At sioctl.sys’s DriverEntry, 1) set the instruction pointer (Eip) thus;
2) set the return code (Eax) thus; 3) set the stack pointer (Esp) thus; 4) announce
that DriverEntry has been entered; and 5) proceed. (Of course, this technique
merely removes the possibility of DriverEntry causing a blowup such as an access
violation. If the operating system expects the driver to be supplying function, that
function is not going to be available, and down the road there may be other
problems.)
kd> r
long -2119659752
kd> dd &ntStatus l1
f88fac78 81a88f18
In this case, the form @ecx should be employed, to ensure WinDbg knows you’re
referring to a register.
There are more registers than those shown by default. To see the full complement,
use the rM command (“M” must be uppercase; this is actually the r command with
the parameter M, with no space allowed between command and parameter):
kd> rM 0xff
mm0=c3d2e1f010325476 mm1=0000ffdff1200000
mm2=000000018168d902 mm3=f33cffdff1200000
mm4=804efc868056f170 mm5=7430804efb880000
mm6=ff02740200000000 mm7=f1a48056f1020000
xmm3=-7.98511e-039 0 0 -7.98504e-039
• If you don’t like to use commands to change things, you can bring up a memory
window (ViewàMemory), variable window (ViewàLocals) or register window
(ViewàRegisters) and overtype values as you like. For example,
Controlling execution
Earlier (see IoCreateDevice) you were asked to let execution proceed from one point to the
next, without being told how. There are several ways to control execution. All the below,
except the first, assume that execution is currently halted.
• Step over (F10) — This causes execution to proceed one statement (if C or C++ and
WinDbg is in “source mode,” toggled by DebugàSource Mode) or one instruction
at a time, with the provision that if a call is encountered, execution passes over the
call without entering the called routine.
• Step in (F11) — This is like step-over, except that execution of a call does go into
the called routine.
• Step out (SHIFT-F11) — This causes execution to run to an exit from the current
routine (current place in the call stack). Useful if you’ve seen enough of the routine.
• Set instruction to current line (CTRL-SHIFT-I) — In a source window, you can put the
cursor on a line, do that keystroke sequence, and execution will start from that point
as soon as you let it proceed (e.g., F5 or F10). This is handy if you want to retry a
sequence. But it requires some care. For example, registers and variables are not set
to what they would be if execution had reached that line naturally.
• Direct setting of Eip — You can put a value into the Eip register, and as soon as you
hit F5 (or F10 or whatever), execution commences from that address. As should be
obvious, this is like setting instruction to the cursor-designated current line, except
that you specify the address of an Assembly instruction.
At almost any point in execution, there is an area of storage that is used as the stack; the
stack is where local state, including parameters and return addresses, is saved. There is a
kernel stack, if execution is in kernel space, and a user stack for execution in user space.
When you hit a breakpoint, it is likely there will be several routines on the current stack,
and there can be quite a few. For example, if instruction has stopped because of a
breakpoint in the routine PrintIrpInfo in sioctl.sys, use k (“Stack Backtrace”):
kd> k
ChildEBP RetAddr
The topmost line (newest) is the stack frame where control stopped. You can see earlier
callers, too, but if you don’t have symbols, they may not be represented correctly. Since
you enjoy access to the symbols for sioctl.sys, you are presented with file and line number
information for each frame involving the driver.
You can double-click on an entry and be taken to the source file, if that source file can be
located.
If you are interested only in variables pertaining to a routine on the stack, you can make
the routine current by double-clicking on its line above, or you can do kn (a sibling of k)
and then .frame. For example, to get information about the dispatch routine that called
PrintIrpInfo:
kd> kn
# ChildEBP RetAddr
Having set the frame number, you’re able to display (or change, if you wish) variables
known in that frame and registers belonging to that frame:
kd> dv
DeviceObject = 0x80f895e8
Irp = 0x820572a8
outBufLength = 0x64
irpSp = 0x82057318
ntStatus = 0
mdl = 0x00000000
inBufLength = 0x3c
datalen = 0x26
kd> r
SIoctl!PrintIrpInfo+0x6:
kd> x sioctl!*ioctl*
This means, Tell me all the symbols in the module sioctl that contain “ioctl.”
That may seem trivial. Consider, however, this message seen in the debugger in an actual
support case:
On a guess that the routine PopPolicyWorkerAction is in ntoskrnl, you might look there:
kd> x nt!PopPolicy*
With that information, you might put a breakpoint in the routine shown in red.
Selected techniques
That covers fundamental operations. Although the overarching focus here is not on how to
investigate particular areas, there are nonetheless a number of substantive debugger
commands — technically, they are extensions and are written as DLLs — as well as
techniques that deserve mention because they are used over and over in many areas.
kd> !process
Image: ioctlapp.exe
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
PeakWorkingSetSize 263
VirtualSize 6 Mb
PageFaultCount 259
MemoryPriority BACKGROUND
BasePriority 8
CommitCharge 48
The addresses of the process block (EPROCESS) and thread block (ETHREAD) have been
marked in red. You could use these in a conditional breakpoint.
kd> !process 0 0
Image: System
Image: smss.exe
Image: csrss.exe
...
Image: System
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
PeakWorkingSetSize 497
VirtualSize 1 Mb
PeakVirtualSize 2 Mb
PageFaultCount 4179
MemoryPriority BACKGROUND
BasePriority 8
CommitCharge 7
80580040 SynchronizationEvent
80581140 NotificationTimer
80582d80 QueueObject
80582d80 QueueObject
...
To see a particular thread in maximum detail, use !thread with 0xFF as the detail
parameter:
80580040 SynchronizationEvent
80581140 NotificationTimer
Not impersonating
DeviceMap e1002868
UserTime 00:00:00.0000
KernelTime 00:00:03.0406
Stack Init f88b3000 Current f88b2780 Base f88b3000 Limit f88b0000 Call
0
ChildEBP RetAddr
If you are writing a driver, you will often be looking at device stacks. You might begin by
finding devices belonging to a certain driver and then examine the stack a device is in.
Suppose you are interested in the ScsiPort miniport driver aic78xx.sys. Start with !drvobj:
\Driver\aic78xx
(f8386480 8267da38)
There are four device objects here. Use To look at the first, use !devobj to get some
information about the device and !devstack to show the device-object stack to which the
device object belongs:
ExtensionFlags (0000000000)
!DevNode 8263cdc8 :
DeviceInst is
"SCSI\Disk&Ven_QUANTUM&Prod_VIKING_II_4.5WLS&Rev_5520\5&375eb691&1&010"
ServiceName is "disk"
IRPs
The most common method of communication amongst drivers is the I/O Request Packet, or
IRP. To see an IRP’s I/O completion stack, as for example at
Sioctl!SioctlDeviceControl+0x103:
\Driver\SIoctl
To get the full IRP plus its stack, ask for detail:
Flags = 00000070
ThreadListEntry.Flink = ff70fbd0
ThreadListEntry.Blink = ff70fbd0
IoStatus.Status = 00000000
IoStatus.Information = 00000000
RequestorMode = 00000001
Cancel = 00
CancelIrql = 0
UserIosb = 0006fdc0
UserEvent = 00000000
Overlay.AsynchronousParameters.UserApcRoutine = 00000000
Overlay.AsynchronousParameters.UserApcContext = 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
\Driver\SIoctl
+0x000 Type : 6
IRQL
A command (available for targets that are Windows Server 2003 or later) for occasional use
is !irql, because it shows the current IRQL on the concerned processor. Breaking at
Sioctl!SioctlDeviceControl+0x0:
kd> !irql
For an example of a higher IRQL, suppose you added the following code (in curly braces) to
Sioctl!SioctlDeviceControl just before the break statement at the end of the
IOCTL_SIOCTL_METHOD_BUFFERED clause:
Irp->IoStatus.Information = (outBufLength<datalen?outBufLength:datalen);
KIRQL saveIrql;
ULONG i = 0;
i++;
KeLowerIrql(saveIrql);
break;
Now, if you set a breakpoint at the statement after KeRaiseIrql and hit that breakpoint:
kd> !irql
Note, by the way, that the !pcr command will not ordinarily show the IRQL you’re interested
in, namely, the IRQL in force at the point a breakpoint caused a halt.
Dump Files
There is little to say about dump files that is peculiar to them. A few things are worth
mentioning.
• There are three kinds of kernel dump files. A full-memory dump is best, but the
somewhat less voluminous kernel dump suffices for most purposes. There is also the
small-memory dump, which is 64KB in size (and so will be generated more quickly
than the two other types). Since a small-memory dump does not have full
information about executables, it may be necessary to employ the .exepath
command to point to executable images if investigating them is required. Windows
can be configured to create one of these dump files if a crash (bug check) occurs.
• To investigate a dump file, start WinDbg but do not specify a protocol for a target
system. When in WinDbg, open the dump file with FileàOpen Crash Dump. It will
help if the symbol path and possibly the source path are already set.
• Now, in the WinDbg command window, do !analyze –v, to get a summary. The
command will probably suggest an execution context (.cxr); setting that context will
give you access to the call stack at the time the bug check was issued (which,
hopefully, will be close to the actual error). You may need to proceed to processes
and threads (!process and !thread), to look at the list of kernel modules (lmnt),
from that list to look at selected driver objects (!drvobj) and possibly to look at
device nodes (!devnode), device objects (!devobj) and device stacks (!devstack).
But beyond !analyze –v, there are no simple prescriptions in working with a dump
file.
If a kernel-mode dump file has been created after a bug check has occurred, debugging this
file is similar to debugging a bug check that occurs when a debugger is attached. The
Here is how to begin analyzing a bug check. In this example, the kernel debugger is
attached when the crash occurs, but the procedure when analyzing a kernel-mode dump file
is similar.
In this example, the sample driver Sioctl.sys is loaded, and a breakpoint is set at
Sioctl!DriverEntry. When the debugger stops at this breakpoint, set Eip to 0. This is never
a valid value, since the instruction pointer cannot be zero. Then let execution proceed via
F5. A kernel error will occur, and a bug check will be issued. You can then use
the !analyze extension command to investigate:
kd> !analyze -v
***********************************************************************
********
*
*
* Bugcheck Analysis
*
*
*
***********************************************************************
********
SYSTEM_THREAD_EXCEPTION_NOT_HANDLED (7e)
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:
------------------
FAULTING_IP:
+0
00000000 ?? ???
ExceptionAddress: 00000000
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000000
Parameter[1]: 00000000
00000000 ?? ???
CURRENT_IRQL: 0
READ_ADDRESS: 00000000
BUGCHECK_STR: 0x7E
STACK_TEXT:
FAILED_INSTRUCTION_ADDRESS:
+0
00000000 ?? ???
nt!IopLoadDriver+5e1
SYMBOL_STACK_INDEX: 1
SYMBOL_NAME: nt!IopLoadDriver+5e1
MODULE_NAME: nt
IMAGE_NAME: ntoskrnl.exe
DEBUG_FLR_IMAGE_TIMESTAMP: 3e800a79
FAILURE_BUCKET_ID: 0x7E_NULL_IP_nt!IopLoadDriver+5e1
BUCKET_ID: 0x7E_NULL_IP_nt!IopLoadDriver+5e1
00000000 ?? ???
kd> kb
The topmost entry in the above stack looks wrong. That is something you might encounter
in a dump file. How would you work with that if you didn’t know how the bug check had
been produced?
1. Do .frame 1, to get to the frame for the caller of the mystery routine,
nt!IopLoadDriver.
3. The call was to the address in a doubleword pointed to by the Edi register’s value
plus 0x2C. That is the address you need. So, display the Edi register:
kd> r edi
edi=81a2bb18
4. A little arithmetic:
kd> ? 81a2bb18+0x2c
kd> dd 81a2bb44 l1
kd> dt f87941a3
GsDriverEntry
SIoctl!GsDriverEntry+0(
_DRIVER_OBJECT*,
_UNICODE_STRING*)
Thus you would have determined the real routine at the top of the stack.
Pseudo-registers
Pseudo-registers are variables that you can employ for various purposes. A number of
pseudo-registers are predefined in meaning: $ra for the return point in the current entry on
the call stack, $ip for the instruction pointer, $scopeip for the address of the current scope
(local context, which makes available local variables in the current routine), $proc that
points to the current EPROCESS, and so forth. These might be useful in conditional
statements.
There are also pseudo-registers with operator-defined meaning, $t0 through $t19. These
can be turned to various purposes, like counting breakpoints. A pseudo-register was put to
that use in a real-life case where there were many updates to a piece of storage:
The approximate meaning of the above is, When the doubleword at 0x81B404D8 is updated,
update the pseudo-register $t0 as a hit counter, say what is the number of the hit and show
the value at 0x81B404D8, the call stack, the current process and the current thread. (To
understand details in the above, refer to aliasing below.)
Another illustrative use is from a support case where it was necessary to track the activity
of Atapi.sys’s DPC routine (Atapi.sys is a standard operating system driver). This routine
was entered extremely often, but the investigating engineer knew that at a specific point
there would be an interesting IRP about to be completed, and the variable irp pointed to
that IRP. In another routine in Tape.sys (another standard operating system driver), there
was a variable named Irp that pointed to the same IRP. The engineer wanted to stop in
Tape.sys at just the right time, so he started by setting a one-time breakpoint in the
Atapi.sys DPC:
When that one-time breakpoint was hit, the engineer did this:
This means: When this second breakpoint in Tape.sys is hit, if the local variable Irp matches
$t0, put out some eye-catching information and display the value of Irp. If, on the other
hand, Irp does not match $t0, just go. When this second breakpoint caused execution to
stop, control had halted where the engineer desired.
Aliasing
It may be convenient to replace a set of characters with another set in a command string.
One use is to define a short string for a long set of commands. For example,
kd> al
Alias Value
------- -------
kd> demo
kd> Demo
nt!RtlpBreakWithStatusInstruction:
804df1c0 cc int 3
ChildEBP RetAddr
If you look back at pseudo-registers, you will now recognize that alias commands were used
in the first example there. To begin, suppose that this was the actual command:
Here’s what would be put out in the WinDbg command window, with the effect of aliasing
shown in red:
hit # 0x1
ChildEBP RetAddr
Image: ioctlapp.exe
...
hit # 0x2
...
hit # 0x3
...
The basic trick above is to define the command block associated with hitting the breakpoint
in such a way that the imbedded alias definition isn’t evaluated immediately but only when
a breakpoint is hit. That is achieved by using the ${} (“Alias Interpreter”) token with the /v
option to specify an alias not evaluated at specification (in the bp command, that is) and
the .block (“Block”) token to cause alias evaluation at the time of hitting the breakpoint
and executing the associated commands. Finally, the /x option of the as command ensures
64-bit values are used, and the ad ensures the latest alias is cleaned up.
1. Use !irpfind (see the Debugging Tools for Windows help file) to find IRPs in the non-
paged pool. You will get lines like this:
3. In the file, select all lines containing xStor and put those lines into another file,
debugtst1.txt. That yields lines like this one:
!irp fffffadfe5e4c9d0 1
The !irp extension displays the IRP at the given address, including the IRP’s major
fields and its stack. Save debugtst1.txt.
5. Now, in WinDbg, give the command $$<c:\temp\debugtst1.txt. You will get a lot
of output, starting with:
1: kd> $$<c:\temp\debugtst1.txt
Flags = 00000000
ThreadListEntry.Flink = fffffadfe5e4c9f0
ThreadListEntry.Blink = fffffadfe5e4c9f0
IoStatus.Status = c00000bb
IoStatus.Information = 00000000
...
Tail.Apc = 0326cc00
Tail.CompletionKey = 0326cc00
\Driver\xStor CLASSPNP!TransferPktComplete
The value in red is, under these conditions, the SRB address for that IRP.
6. To get the SRBs formatted, create debugtst2.txt by finding and copying all lines in
the above output that contain ‘Args: ffff’. Then change each line to be like this:
dt nt!SCSI_REQUEST_BLOCK fffffadfe6869130
Note: Since the Microsoft symbol store contains only "public symbols", so
nt!SCSI_REQUEST_BLOCK may not be available. For present purposes, think of it
simply as a structure defined in a driver for which you have complete symbols.
...
+0x044 Reserved : 0
Thus, with a few minutes’ work, you would have found and displayed all the SRBs of interest.
It is possible to write a debugger extension to do the same thing, but for a one-off
investigation, a simple script may be the better approach. You can take a script a step
further by packaging commands with control logic to form a command program. The control
A debugger extension is even more powerful, but it takes more time to write. An extension,
written in C or C++ and built as a DLL, has available the full power of the debugger and its
“engine.” Many of commonly used commands such as !process are in fact extensions. The
specifics of writing an extension are beyond the present scope. Refer to “Debugger
Extensions” in the Debugging Tools for Windows help file.
Remote debugging
WinDbg (and KD) can be connected to a target to act as a server for a debugger instance
acting as client, via TCP/IP or other protocol. A test system is connected via COMx or 1394
to a debugger, and the debugger is made into a server; then a developer can investigate
problems or exercise function at a distance. This setup is very valuable in automated testing,
allowing a person to investigate lab problems from his or her desk.
To get this capability, you can start the debugger with a command-line option to indicate its
role:
.server tcp:port=5005
Either method causes WinDbg to act as a debugging server, listening at TCP/IP port 5005.
.tcp:server=myserver,port=5005
• If there is a firewall between the host system in one local or corporate network and
the target system in another network, remote debugging is more complicated. See
the Debugging Tools for Windows help file for details.
• Access to symbols and to source are based on the permissions of the person logged
in at the remote server, not the permissions of the person that is running the client.
WinDbg does its best to figure out the call stack, but sometimes it is defeated. Retrieving
such a situation is one of the most difficult tasks facing the person who is debugging, since
she or he must use her/his own knowledge to supplement WinDbg’s expert knowledge. Be
warned, then! Tough slogging ahead.
Consider this example from a dump file where there was a double-fault bug check
(unexpected kernel-mode trap with first argument 0x00000008):
kd> k
ChildEBP RetAddr
It would seem there was only one routine on the stack, an operating system clock-interrupt
routine. It is a bit suspicious that that should have failed. So, begin by looking at the
current thread:
kd> !thread
...
Stack Init be810000 Current be80fd34 Base be810000 Limit be80d000 Call
0
The stack starts at 0xBE810000 and goes down to 0xBE80D000 (three pages, which is
normal). The stack base (ChildEBP) for the apparently failing clock routine is 0xBE80CFF8,
which is beyond (below) the stack’s end. Is it likely that a clock routine would have used
more than the standard stack?
The detective work begins with looking for addresses in the stack that may indicate other
routines. For this the usual tool is dds (“Display Words and Symbols”), to look for stored
addresses (dqs and dps can be used, too; note that all three of these commands are case-
sensitive). For present purposes ignore the clock-interrupt routine and instead focus on the
stack up to but not including the routine interrupted by the clock routine. But don’t ignore
Next look at 0xBE80CFF8 and see if anything interesting shows up (annotations below are
shown with C-style comment delimiters):
be80d000 00000000
...
be80d034 00000000
be80d038 00000020
...
be80d058 be80d084
be80d05c 00000000
be80d064 00000008
be80d068 00000246
be80d06c 8a61c004
be80d070 bfbb7858
be80d074 88c1da28
be80d078 00000000
be80d07c 00000000
be80d080 00000000
...
Assume that the line with the identification “zzzndx+0x103E0” is the driver routine
interrupted by the clock routine. You will notice the earlier line (higher stack address) with
the identification “zzznds+0xBED7.”
Now look at disassembled instructions a little before zzznds+0xBED7 (the point of call):
zzznds+0xbed0:
Note that the call is to zzznds+0x101BE, which is fairly close to the first identified line. Thus,
the disassembly could well be the call to that.
Looking back to the output from dds above, you can see at 0xBE80D088 the caller’s saved
Ebp. But the operations of pushing down that Ebp (push ebp at 0xBFEE01BE) and of saving
it at 0xBE80D088 mean that Esp after the pushing is 0xBE80D084, and since Esp becomes
the current Ebp (instruction at 0xBFEE01BF), and since 0xC is subtracted from Esp at
0xBFEE01C1, the resulting Esp value at the instruction at 0XBFEE01C4 has to be
0xBE80D078.
Now you have determined the Ebp, Esp and Eip values for what was called by
zzznds+0xBED7, namely, 0xBE80D084, 0xBE80D078 and 0xBFEE01C4, so you supply them
to the k command for it to use them rather than try to discover values:
...
That is only part, the latest part, of the stack (there was more). But you should have the
general idea.
To arrive at the parameters given to k immediately above, a good deal of detective work
was necessary, involving a search of the stack and looking at code to see how the stack is
built at a certain point. The work will vary from case to case. The lesson here is that if
WinDbg’s stack backtrace looks short, see if the kernel stack allotted to the failing thread is
plausibly accounted for. If not, begin to dig in.
If you spend an extended period of time in stepping through kernel code (with F10 or F11,
for example), you may notice that control suddenly jumps to a point you didn’t expect. This
is the more likely if the code is running at an IRQL less than DISPATCH_LEVEL and you
employ step-over (F10). If you knew that you were following control running under a
There is no simple way of dealing with this behavior, which, to repeat, is expected. What
can work for you is to be on the watch for unexpected jumps in stepping, and to check the
current thread if you are suspicious. If you find the thread has switched, you should look
back to find the last good point, restart the testing from square 1, set a one-time thread-
qualified breakpoint at that last good point, and let things run until that breakpoint is
reached. Then you can proceed: You are still exposed to switches, but you are that much
further along the path of interest.
This brief essay did not plumb all the debugger’s possibilities. For more information, consult
the Debugging Tools for Windows help file. If you still have unanswered questions, bring
them up in the public newsgroup microsoft.public.windbg (hosted by
msnews.microsoft.com), or send email to windbgfb@microsoft.com.