Introduction to WinDBG
When nothing goes wrong and Visual Studio can no longer help us, we still have one last tool that may allow us to save ourselves, as long as we are ready to roll up our sleeves: WinDBG. This article is an introduction to this tool.
Visual Studio has evolved a lot since the era of version 6, but not always to the benefit of C and C++ developers. After the golden age of .net functionality, we are now seeing a return to the forefront of C++ programming with the rejuvenation of the language since the C++11 version. Our IDE of choice is catching up, but it's still lagging behind when it comes to getting its hands dirty and finding the source of a "real" bug. The kind of bug that doesn't take the time to politely warn you with a class of exception to let you know what's wrong before it bursts in your face. The kind of bug that takes the trouble to corrupt the call stack before giving you a chance to debug it. Maybe the value of the pointer this was hidden when the compiler optimized the program?
When everything goes wrong and Visual Studio can't help us anymore, we still have one last tool that may save us from certain death (I'm hardly exaggerating), as long as we are ready to roll up our sleeves: WinDBG.
What is WinDBG
WinDBG is a GUI over the kd kernel debuggers, cdb user and ntsd user, which means that it can debug code that runs at the operating system kernel level (such as device drivers) as well as applications launched by the operating system.
Since these two modes of operation are rather different in their operation, this article will focus on the mode of operation that is most likely to be useful for the common programmer: the user mode.
WinDBG is now distributed with the Windows SDK (current version: Windows SDK for Windows 8.1). It should be noted that although one must use the SDK installer, it is not necessary to install the SDK itself to have access to the debugging tools distributed with it. All we need to do is select Debugging Tools for Windows and Application Verifier during the installation, which alone contain everything we need.
There is one version of WinDBG for applications compiled for the x86 architecture and another one for the x64 architecture. Note that a 64-bit application must be debugged with the x64 version of the application and that although it is possible to debug a 32-bit application with the x64 version of WinDBG, it is more complex. So I suggest limiting the sources of headaches and using the appropriate debugger for the task.
In order to use WinDBG effectively, it is important to always make sure that a certain minimum configuration is performed, otherwise the quality of the information revealed by the debugger will be limited.
The main configuration to perform is to make sure that WinDBG knows where to find the symbols of the application to be debugged, the Windows public symbols and any other symbol that might contain useful information for our problem. For example, if we use an external library and we suspect that it could be responsible for our problem, it would be useful to have access to its debugging symbols, if available (which is rarely the case for proprietary applications).
In the symbol configuration menu (File/Symbol File Path), we first add the symbols of our application to be debugged by clicking on the Browse... button and pointing to the directory containing the *.pdb files we are interested in. If there are several directories of interest, just repeat the operation for each folder, and their respective path will be added to the global list.
To add the Windows symbols, simply point to the Microsoft public symbol server and add the following text after the list:
(here c:\symbols is a directory where downloaded symbols will be cached; it is possible to choose a different one).
Different types of debugging sessions
When you want to find the source of a bug by directly executing the faulty program, this is called live debugging. This method is preferable because it gives access to a wider range of data about the process in progress. However, some bugs are particularly difficult to reproduce. Either a random component makes replicating the problem long and arduous, or the problem only occurs on a specific machine that is not accessible. This is why there is another type of debugging session:
Analyze the crash dump
When a hard-to-replicate bug arrives, a smart tester will generate a crash dump of the faulty application to take a picture of the status of the process for future reference. This way, even if we have only reproduced the problem once, we can search the state of the program repeatedly in different ways to try to find the source of our problem. That said, since this type of debugging is done post-mortem, some types of data are not available (any transient data that has not been previously recorded is lost, for example), which can make the analysis a little more complicated.
Apart from a few basic features that are available in its graphical interface (call stack, thread list, list of local variables, etc...), the vast majority of commands used in WinDBG are text commands. For example, to display on screen the call stack of the current thread, you can use kb.
Here is a list of several commands useful in a variety of different scenarios (we will be able to explain some more elaborate debugging techniques in a future article) :
- !analyze -v
- Meta-command that tries to find the source of a bug automatically (rarely works, but it doesn't cost much to try)
- Displays the call stack of all threads having a unique call stack (hides duplicates)
- dt -b
- Forces the interpretation of what is at the address mentioned as being of symbol type, as defined in the binary module and displays the value of its members (for example, allows to display a void* as a CObject*)
- Displays the TEB (Thread Environment Block) data structure associated with the current thread. Useful among others to determine the stack limits of the thread in memory (Stack Base, Stack Limit).
- (32-bit) or
- (64-bit) Displays all memory contents between address1 and address2 as well as the symbol associated with each address, if any.
- Displays the range of assembler instructions preceding a given address (e.g. ub 0x333333 L200 displays the 200 instructions preceding address 0x333333). Useful for example to deduce what was placed on the stack before a function call.
- !address -summary
- Displays a table showing how the process memory space is distributed.
- If an exception has been thrown and its context is still present in memory, this displays the context of the exception (the state of the registers and the faulty instruction) and puts the debugger in the same state as when the exception was thrown.
To load the extensions needed for debugging managed :
- sxe ld clr
- Adds a breakpoint that will suspend the application once CLR.dll is in memory (prerequisite for the following steps)
- .loadby sos clr
- Loads extensions for managed debugging
- .load sosex
- Loads additional extensions for managed debugging (requires the prior installation of these extensions from the developer's site)
- Displays the combined call stack of interleaved managed and unmanaged calls
- Displays the list of all managed objects allocated on the current stack
- Displays the details of a particular managed object located at address
There are no rules on how to debug with WinDBG. The reality is that there are as many methods as there are programmers. We will visit in a future article some techniques that allow faster results with different types of problems (crashes, application freeze, etc.), but none of these techniques is a guarantee of success. Only one thing is certain, the more you will use WinDBG, the more you will learn about the internal workings of Windows applications and the Windows operating system, which in itself is a sure success!