3.5 Advanced Debugging Techniques

So far we have looked at debugging relatively straightforward solutions. Visual Studio .NET is capable of debugging multiple solutions simultaneously, even when those solutions span multiple threads, processes, languages, technologies, and even multiple machines. While such projects require a little more configuration, it is much easier to debug these scenarios than it was with previous versions of Visual Studio.

3.5.1 Crossing Language and Technology Boundaries

The .NET runtime is often referred to as the CLRthe Common Language Runtime. It is so called because all languages that target the .NET platform share the same runtime environment. One of the benefits of this is unified debugging. If your solutions contain components written in multiple languages, then as long as those components have all been built with debug support enabled, traversing language boundaries works seamlessly. No special configuration is required.

When crossing technology boundaries, however, you will need to make sure that things are set up correctly before you start. So if your system contains a mixture of .NET and native Win32 code, you will need to ensure that your startup project's configuration enables both types of debugging, as described earlier. (Or if you are attaching to an existing process, you must make sure that both the Common Language Runtime and the Native options are checked in the Attach to Process dialog.)

T-SQL is a special case. You can set breakpoints in stored procedures and step through T-SQL code just like other languages. However, T-SQL is different, in that you cannot step directly into it from another language.[4] Stored procedures are usually invoked through some data access API, such as ADO.NET or OLE DB, using code such as that shown in Example 3-3.

[4] We may be able to do this in the future. The upcoming "Yukon" is slated to have much tighter integration of the .NET runtime and SQL Server stored procedures.

Example 3-3. Calling a stored procedure from C#
. . .

cmd.CommandType = CommandType.StoredProcedure;

IDataReader dr = cmd.ExecuteReader(  );

. . .

Unfortunately, if you try to step into (F11) such a line of code, Visual Studio .NET will ignore you. It is not smart enough to realize that this code is executing a stored procedure on a SQL Server database. To debug the stored procedure, you must therefore set a breakpoint in the T-SQL itself. You can open the stored procedure from the Server Explorer windowjust locate the relevant SQL Server database and expand its Stored Procedures node. If you double-click on a stored procedure, Visual Studio .NET will open its source code. (This feature is available only on Enterprise editions of VS.NET.) You can set breakpoints in this code just as with any other code.

3.5.2 Multiple Threads

When Visual Studio .NET suspends a process during debugging, it halts all of the threads. You can look at only one thread's state and call stack at a time, but it is possible to switch to other threads in the process and examine those using the Threads window. You can display the Threads window using Debug Windows Threads (Ctrl-Alt-H).

The Threads window, shown in Figure 3-32, shows all of the threads in the target process. It indicates the one currently selected for debugging by highlighting it with a yellow arrow. For each thread, it shows the thread ID, the thread name, the function in which the thread is currently executing, the thread's priority, and whether it is suspended. The function name will often be blank when the code is executing a system call. For example, the worker thread in Figure 3-32 is actually inside the Thread.Sleep method.

Figure 3-32. The Threads window

In .NET applications, you can set a thread's name using the Thread class's Name property. In native Win32 applications, you must use a slightly curious hackyou raise SEH (Structured Exception Handling) exception number 0x406D1388, passing in a pointer to a THREADNAME_INFO structure. The Visual Studio .NET documentation provides sample code for this in the "SetThreadName function" help entry.

Visual Studio .NET allows you to suspend individual threads manually in the debugger. The Threads window's context menu has a Freeze option, which will prevent the selected thread from running when you allow the program's execution to continue. (The context menu for a frozen thread has a corresponding Thaw option, which will allow the thread to continue.)

Freezing threads can occasionally be useful when single-stepping through code. Every time you step through a line of code, all of the other threads in the system will be allowed to run for a short while too. If you have breakpoints set elsewhere in your code, this can be inconvenientif some thread other than the one that you are single-stepping with hits a breakpoint, Visual Studio .NET will switch to that thread. This can be somewhat disorientating. You can avoid this by temporarily freezing all of the threads other than the one you wish to examine.

3.5.3 Multiple Processes

Visual Studio .NET can attach to any number of processes in a single debugging session. The simplest way to exploit this is to use the Processes window (Debug Processes...) described earlier (see Figure 3-1). This dialog can be opened even when a debugging session is already in progress, and you can simply add more processes to the list. Also, if you are using a technology that supports cross-process method calls such as COM or .NET Remoting, you will then be able to step into (F11) code across process boundaries.

For some projects (especially those involving remoting), you may need to launch a particular set of processes and then attach to them every time you debug. It can be tedious to use the Processes dialog for this. Fortunately, a Visual Studio .NET solution can be configured to launch several processes and attach the debugger to all of them whenever you use Debug Start (F5).

If you right-click on your solution in the Solution Explorer (be sure to click on the solution itself, not one of its projects), you will see a Set Startup Projects... item. This displays the Startup project page in the solution's property pages, as Figure 3-33 shows. If your solution contains multiple projects, you can select the Multiple Startup Projects radio button and configure any or all of the projects in your solution to be run when debugging starts. As the drop-down list shows, you can also choose to start a project without attaching the debugger. You can control the start order tooprojects will be started in the order in which they appear, and you can change this with the Move Up and Move Down buttons.

Figure 3-33. The Solution startup projects page

3.5.4 Cross-Machine Debugging

Debugging processes on multiple machines in Visual Studio .NET is almost as easy as debugging multiple processes on a single machine. The only restrictions are that the remote machine must have the appropriate remote debugging support installed, you must have the appropriate DCOM and security settings on the remote machine, and you cannot launch remote processes automatically when you start debugging.

If the target machine has Visual Studio .NET installed, you do not need to install any extra software. But if it does not have Visual Studio .NET installed, you can instead install the Remote Debugging Components. (These components can be installed from the Visual Studio .NET installation disks.) The Remote Debugging Components install just enough functionality to allow code to be debugged remotely.

Remote debugging relies on DCOM, so you may need to adjust the DCOM settings on the target machine before remote debugging will work. You can use the dcomcnfg utility to grant developers permission to use DCOM. In Windows XP, you do this by expanding the Component Services node in dcomcnfg, locating the computer you wish to configure, and selecting propertiesthis will display the DCOM properties window for your computer. Under Windows 2000, this window will appear as soon as you run dcomcnfg. From this dialog, select the Default COM Security tab and click on the Edit Default... button in the Access Permissions section. Make sure that any developers who require access are listed here. Also, make sure that the SYSTEM account is listed.

Finally, the developers will need to be a member of either the Debugger User group or the local Administrator group on the target machine.

Once the remote machine has the appropriate software installed and the security and DCOM settings are configured correctly, you can attach the VS.NET debugger to processes on that remote machine. Simply type the machine's name into the Name field of the Processes dialog, or select the machine from the "..." button. The dialog will show a list of processes running on the remote machine, and from there on, everything works in much the same way as it does for local debugging. T-SQL debugging

VS.NET is able to debug SQL Server-stored procedures, but to use this feature, you will need to make sure that your systems are configured appropriately. If you are running SQL Server locally (i.e., on the same machine as you are running VS.NET), you will usually find that it just works out of the box but in distributed scenarios a little more work may be required.

The VS.NET remote debugging components must be installed on the server machine as described earlier. You will also need to make sure that security and DCOM are configured appropriately, just as you would for normal remote debugging. (If you make any changes to the DCOM settings, you must restart SQL Server for the changes to take effect.)

You must also make sure you have the appropriate SQL security configuration. The only requirement here is that the developer is able to call the sp_sdidebug stored procedure. Use SQL Server Enterprise Manager to grant the developer access to this procedure. (The related mssdi98.dll component must also be installed in SQL Server's bin directory in order for this stored procedure to work.)

3.5.5 Alternative Debugging Protocols

The remote debugging features of Visual Studio .NET use DCOM to communicate with the target machine. Unfortunately, in certain network configurations, it may be awkward or even impossible to use DCOM. Also, DCOM debugging is not supported when the target machine is running Windows 9x, Windows ME, or the Home Edition of Windows XP. VS.NET therefore supports two other protocols, although with some loss of functionality; they are named pipes and TCP/IP. (VS.NET 2002 does not support named pipes.)

Named pipes and TCP/IP are less secure than DCOM. (The documentation is not precise about what this meansit merely says that pipes are less secure than DCOM and TCP/IP is less secure than pipes.) These protocols also support only native debuggingto debug managed code, T-SQL, or script, you must use the default DCOM protocol. So you should resort to named pipes or TCP/IP only if you have no other choice.

To use named pipes or TCP/IP, you must run the Remote Debug Monitor on the target machine. This is installed as part of the remote debugging setup described earlier, but it is not a service; it is a console application called msvcmon.exe and must be started manuallyit is not left running by default due to the lower security offered by these transports. It can be found under Visual Studio .NET's Tools menu in the Start menu as the Visual C++ Remote Debugger item. (Or you can run the same program from the command line, although unlike Visual C++ 6, Visual Studio .NET does not install the program on the path, so you must find it first. It is usually in the Common7\Packages\Debugger subdirectory of the VS.NET installation.)

By default, msvcmon.exe accepts only named pipe connections. You must run it from the command line with the -tcpip option.

Once the Remote Debug Monitor is running on the target, you can select one of the alternate protocols in the debugger. You attach the debugger using the Process dialog ( see Figure 3-1 ) as usual, but you can select either Pipe or TCP/IP from the Transport drop-down list at the top of the dialog. You must then specify the name of the machine to which you wish to connect as usual, and debugging will proceed as normal (except that only native debugging will work).

3.5.6 Symbol Servers

The Windows Platform SDK ships with a set of tools designed to allow debugging symbols to be distributed from a central server. VS.NET is able to make use of these tools when debugging applications. This can be very useful if you are working on a large project. It enables you to ensure that you are always debugging with up-to-date symbols, without having to ship complete copies of all the debug symbols with each distribution of binaries.

There are two parts to the symbol server technology: the symbol server store manager (symstore.exe) and a client DLL (symsrv.dll). symstore.exe is responsible only for maintaining the contents of the store. It does not serve up the files themselvesthis is done with either HTTP, HTTPS, or normal Windows file shares. (So symbol stores can live on either web servers or file servers.) VS.NET 2003 ships with symsrv.dll, so you will not need to install the debugging tools simply in order to access a symbol store. However, if you want to create or maintain a symbol store, you will need to install the Platform SDK as well as Visual Studio .NET.[5]

[5] If you are running VS.NET 2002, not even the client component is installed by default, so you must install the debugging tools and then make sure that symsrv.dll is available to VS.NETyou can do this by copying it from the debugging tools directory into the Common7\IDE directory inside your VS.NET installation. Using a symbol store

You can instruct Visual Studio .NET to use a symbol server by modifying your solution's debugging properties. Right-click on the solution in the Solution Explorer and select Properties, then in the Solution Property Pages, select the Debug Symbol Files item under Common Properties. Add a new path to the list on the right. This path should be of following form:


The first part, symsrv*, indicates to VS.NET that this is not a simple file path, but rather an instruction to use a symbol server DLL. The next part tells VS.NET the name of the client DLL to usesymsrv.dll in this case. (The architecture is designed to allow anyone to write his own symbol clients and servers. symsrv.dll is the client supplied by Microsoft.)

LOCALCACHE should be the path of a local directory, which will be used as a download cache for symbol files. In order to avoid loading symbol files from the symbol server every time the debugger starts, symsrv.dll will copy them into this local directory. The contents of this cache can always be reconstructed from the main server, so if you need to free up some disk space, you can delete the contents of this directory whenever you like. (This will, of course, slow things down a little next time you start debugging but will have the benefit of clearing out any files that you have long since stopped using.)

Symbol stores can store symbol files for many different versions of each binary. So local caches tend to fill up with out-of-date symbols over time. We therefore recommend that you delete the cache from time to timeold symbol files are not deleted automatically.

STOREPATH should be set to the path of the symbol store. This can be a UNC share name or a URL. (Only HTTP and HTTPS URLs are supported.) Consider this example:


This instructs VS.NET to download symbols from Microsoft's symbol server. (Windows debug symbols can be downloaded from here.) It tells it to cache the downloaded symbol files in a local directory called c:\websymbols. This example can be rather useful as it means that symbols for your system DLLs will always be kept up-to-date. However, be aware that this can slow down the debugger quite considerably at startup, especially if you have a slow Internet connection.

Visual Studio .NET 2003 supports an abbreviated form of symbol store path:


The srv* prefix tells it to use the default client DLL, symsrv.dll. The cache and store location are specified in exactly the same way as before.

VS.NET 2002 would download symbol files only from a symbol store path for unmanaged code. If you are using VS.NET 2002, you can still place debug files for managed (.NET) code in symbol stores, but you must place the store path in the _NT_SYMBOL_PATH environment variable rather than configuring it in the solution properties. VS.NET 2003 does not use this environment variable. Creating and maintaining a symbol store

You will, of course, need a symbol store from which to download symbols. The earlier example uses Microsoft's public store, but if you want to use this feature on your own projects, you will need to create a symbol store yourself. All you need is a directory that is accessible either as a file share or via HTTP. You will use the symstore.exe command-line utility to maintain the contents of the directory.

The first parameter to symstore.exe should be add when you are adding files. This can optionally be followed by switches: /r indicates that a directory and its files should be copied recursively. /p specifies that the file will not actually be placed in the store but that the store will merely contain a pointer to the file (i.e., the location of the file). If you specify /p, symstore.exe will usually complain if you attempt to add files with a local path instead of a network pathusually you wouldn't want to do that, since symbol stores are meant to be accessed remotely and local paths will not be meaningful, but the /l switch suppresses this error.

The /l switch can be useful if you want to create a local symbol store on your machine. You may want to do this if you have many projects, all of which use the same set of shared componentsit enables you to put the shared components' debug files in just one place. With local symbol stores, you can also omit the LOCALCACHE part of the symbol store pathsince the store is local, VS.NET has no need to download copies and can just use the files in the store directly.

Next, follow the mandatory switches. /f PATH indicates the file or directory that is to be added. /s STOREPATH indicates the path of the symbol store directory itself. /t PRODUCT and /v VERSION specify the product name and version of the debug information. These should match the corresponding items in the version resource of the binary. (symstore.exe has further options, supporting the generation of index files that can later be used to load symbols into the store automatically. For more information on this, and the internal workings of symstore.exe, consult the Platform SDK documentation.)