Successful debugging is about asking the correct questions. Many (but not all) of those questions can be answered with the Microsoft Visual Studio Debugger. Some questions require advanced debugging techniques and tools. What are the outstanding synchronization objects? How much native versus managed memory has been allocated? What is the size of each generation and the large object heap? These and other advanced questions are not answerable in the Visual Studio Debugger, but may be essential to quickly resolving a debugging problem.
This chapter provides general techniques for debugging managed applications. Some of these techniques are also helpful in debugging Web applications. For guidance on debugging ASP.NET applications, refer to http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/DBGch01.asp.
Advanced debugging tools and services are available from a variety of sources, including the Microsoft Windows operating environment, the Microsoft .NET Framework, Visual Studio .NET, and the Debugging Tools for Windows Web site at Microsoft.com. Performance Monitor, Task Manager, and Dr. Watson are distributed with the Windows environment. The .NET Framework has the Son of Strike (SOS) debugger extension (SOS.dll), DbgClr, CorDbg, and other debugger tools. Visual Studio offers Spy++, Dependency Walker, OLE Viewer, and much more. Advanced debugging tools can be downloaded from the Debugging Tools for Windows Web site at www.microsoft.com/whdc/devtools/debugging. This Web site is updated periodically.
You should download the current tools regularly. The Windows Debugger (WinDbg) and ADPlus script are the most frequently used tools from this Web site.
WinDbg is the primary focus of this chapter. However, it is not intended as a replacement for the Visual Studio Debugger. The first rule of debugging is to look for simple solutions first. The Visual Studio Debugger is ideal for basic debugging: It is convenient, it has a familiar user interface, and superior documentation is available. Visual Studio is a pen hammer, whereas WinDbg is a sledge hammer. If a problem occurs, diagnose the problem first with Visual Studio before attempting a complete reclamation with WinDbg. Find uninitialized variables or parameters, errant loop counters, logic errors, and code coverage problems with the Visual Studio Debugger. These are likely problems in an erroneous application.
The goal of debugging is to resolve abnormal error conditions. Program hangs, crashes, memory leaks, and unhandled exceptions are possible error conditions. Some abnormal conditions are not exceptional events. These primarily include logic errors. For example, a program that reports incorrect results has a bug. It may not be as intrusive as an exception, but it is a bug nonetheless.
Debugging is conducted in three phases: discovery, analysis, and testing. The discovery phase is the process of gathering data related to the problem. In this stage, you capture the state of the application or environment when the error condition occurred. The analysis (or debugging) phase is when the abnormal condition is diagnosed. The testing phase validates the analysis phase. This is an iterative process. Based on the results of the testing phase, further discovery, analysis, and testing could be mandated.
Debugging is an integral part of the life cycle of an application. Debugging a production application is a challenge when compared with debugging in the development environment. First, the constraints are different. The priority for debugging a production application is restarting the application, not exhaustive analysis. With a high-traffic retail Web site, the primary concern of the client might be lost revenue. Second, the production machine might lack debugging resources, such as symbols, source code, and debugging tools. Third, re-creating the abnormal condition could be problematic. Load factors, memory stress, and other conditions are hard to replicate on a developer machine. Finally, the production application may be offsite—in a locked server closet or another otherwise inconvenient location. This might necessitate remote debugging, which could entail setup and possible trust issues.
Debugging can be invasive or noninvasive. Noninvasive debugging is the preference for debugging production applications that are running. The advantages of noninvasive debugging are that the debuggee is not altered or shut down during the debugging process. Invasive debugging provides additional data and flexibility, but should be limited to the development environment.
You can debug live applications or perform postmortem analysis. Live debugging is debugging an active and running application, and it often involves breakpoints. Breakpoints are set where the live application should be interrupted. Starting at the breakpoint, the developer can step through the application to verify program logic, monitor local variables, inspect the heap, watch the call stack, and more. Live debugging usually entails user interaction, which is not always possible. The antithesis to live debugging is postmortem analysis. Postmortem analysis is performed against static data, such as a dump or log file. You can create a dump or log file once and then debug as often as needed. Using a static file is often simpler than live debugging, where the state is constantly evolving. Postmortem analysis also has disadvantages. It is harder to pose future-tense questions with postmortem analysis. What is the call stack after a future operation is performed? What is the effect of further executing a for loop? What is the impact of changing the value of a local variable? Deriving answers to these questions from a dump or log file is indeed difficult.
Production and beta applications are typically release builds, whereas alpha and proof of concepts are often debug builds. Release builds are optimized but harder to debug. Conversely, debug builds are not optimized and run slightly slower. In .NET 1.1, release builds might have incomplete tracking information from the inlining of methods. The missing stack frames could lead to unreliable stack traces in debuggers. This optimization is not performed on .NET 2.0 applications. In addition, there is code optimization, which is a compilation of several performance-improving techniques, including inserting jumps, reducing the lifetime of local references, reordering instructions efficiently, and so on.
This chapter presents different versions of the Store application. Each version demonstrates a different aspect of advanced debugging. The Store applications can be found in the companion Web content.