If you were given a brief to design the Ultimate Debugger, what goals would you aim for? For me, there would be one goal above all else: the ability to single-step through my entire application, no matter what languages are involved and where the code is running. While the Visual Studio .NET debugger does not quite reach this state of nirvana, it comes fairly close.
This chapter is a detailed introduction to some of the features offered by this rather complex system, investigating how it works, what it is capable of, what debugging modes are available, and how to use some of the new debugging windows . This is a necessary precursor to future chapters that will go into more detail on configuring and using the Visual Studio .NET debugger.
For developers accustomed to the VB.Classic debugger, this new version can come as something of a shock . Up until now we've only had the ability to debug a single interpreted language, and even that came with some frustrating limitations. Now we've been gifted with a debugger that is considerably more versatile and more complex. Here are some of the Visual Studio debugger highlights:
A single user interface for all of the languages
Three different debugging modes
Simultaneous debugging of multiple processes across multiple machines
Advanced breakpoints across multiple components
Knowledge of every loaded component, including version and load path
Remote debugging
Direct access to low-level assembly code and to memory
Debugger automation
To give you a taste of these capabilities, I briefly discuss each of them in the sections that follow, before going into more detail later in the chapter.
If you use multiple languages in your application ”for instance, C#, VB.Classic, or SQL in addition to VB .NET ”a single user interface across all of these languages can boost your productivity significantly. It means that you can use the same debugging windows, the same key strokes, and almost the same debugging facilities wherever you go. There is no need to learn a different debugging environment every time you make a language switch. This reinforces the part of the .NET philosophy that says your programming language choices are not as critical in the new world as they were in the old.
The list of languages that the Visual Studio debugger can handle is long, and it includes a couple of surprises :
Any .NET language (e.g., VB .NET, C#, J#, C++ managed extensions)
Native Win32 languages (e.g., VB 5.0 and upward, C++, C)
Transact-SQL (SQL Server 7.0 and upward)
Any ASP .NET language (e.g., VB .NET, C#)
Any ASP language (e.g., VBScript, JScript)
The capability to step seamlessly from one language to another within the same application is one of the more impressive features of the Visual Studio debugger.
The VB.Classic debugger allowed you just one debugging mode. The Visual Studio debugger gives you more choices, and therefore you have more decisions to make. There are three major debugging modes:
Start one or more programs from within the Visual Studio IDE.
Attach the debugger to one or more already-running processes.
Use the Just-In-Time debugger to attach to your application when it crashes.
Because Visual Basic (VB) .NET makes it feasible to build and deploy large distributed systems, the Visual Studio debugger gives you the ability to debug multiple processes running on multiple machines simultaneously . This means that you can sit at a single machine and debug the interactions of every component in your application, no matter where that component lives. If you were trying to find a bug that might be hiding in any part of a distributed application, testing and debugging each part of that application separately would be a nightmare. Now you can analyze and debug your application as a single entity.
This facility also allows you to attach to a program running on an end user's machine, assuming that you have been given the right permissions. In this way you can investigate an end user's program bug as it happens, without having to replicate the problem on your own machine.
The Visual Studio debugger is much more flexible in its use of breakpoints than VB.Classic. The following breakpoint types are available when you use VB .NET:
File : Stops at a line within a specified source file
Function : Stops at the start of a specified function
Address : Stops when instruction at a memory address is reached
Any of these breakpoint types can have additional modifiers whereby it is only triggered when it has been executed a specified number of times or when a specific expression evaluates to true or has changed its value. Other enhancements are the automatic saving of breakpoints from session to session in the solution's .suo file and the ability to disable breakpoints without removing them. Finally, the debugger can generate any necessary child breakpoints automatically; if, for instance, you set a breakpoint on an overloaded method, every overload of that method can be given the same breakpoint.
Many nasty bugs can be traced to versioning problems. The Modules window is designed to show you as much information as possible about every .dll, .exe, and other module loaded by your application. This information includes the module's name , memory address, version, load path, and whether its debug symbols were located. Even the load sequence of the modules is shown.
There are many times when it is very convenient , or even essential, to debug an application running on another machine. This could be on a production machine where Visual Studio cannot be installed locally, a machine whose configuration you don't want to upset, or perhaps you want to debug some window painting code where using a GUI-based debugger locally would be impossible . To do this, you can install a small set of remote debugging components that will allow you to attach and debug remotely while sitting at your own machine. This allows you to debug most problems in place, which is a major step forward for VB.Classic developers.
Some bugs can only be solved with the use of "heavy" debugging, where you have to dive down into low-level code. For this purpose, the Visual Studio debugger provides the Disassembly, Memory, and Registers windows.
The Disassembly window shows you the processor-native code side by side with your source code. You can single-step this native code and even set breakpoints. With an advanced technique, the Disassembly window can also show the native code with its matching Common Intermediate Language (CIL) rather than your source code. This technique is useful if you want to understand how CIL works or if you are trying to solve a CIL problem.
The four Memory windows allow you to peer deep into the memory used by your application and play with what you find there. My suspicion is that most managed code developers will rarely use this window, but it's there for that once- a-year occasion when you really do need it.
The Registers window allows you to examine (and change if you dare) the contents of the CPU's physical registers. Managed code doesn't use the physical CPU registers directly, but instead accesses a set of registers that are maintained by the common language runtime (CLR). These CLR registers can also be viewed and edited.
The Visual Studio debugger has a set of interfaces that can be accessed programmatically. This means that you have access to the debugging state of your application, and therefore you can write macros that use this information. If you open the Macro Explorer window, you can see some sample macros that perform useful tasks such as dumping the call stack of every thread in every program within the current application.