Saturday, December 08, 2007 6:51 PM
jrobbins
Use Two Debuggers at Once
One of my favorite debugging tricks is to get two debuggers on a process at once. Why should you sacrifice the ease of use of Visual Studio for the power of WinDBG? Each of the debuggers has their place and when you're wrestling with that one tough problem I want all the power I can get right at my fingertips. While Visual Studio does a great job at advanced breakpoints for your .NET application, because of a bad design decision in the CLR Debugging API, you can't see what Garbage Collection generation an object is in but you can see that with the SOS and SOSEX extensions. In native debugging, Visual Studio gives you all the ease of use you could want, using the WinDBG !handle command can help you track down handle leaks and deadlocks a lot easier. Using your tools effectively is one of the secrets to debugging faster.
If you're already debugging in native or mixed native and .NET mode with Visual Studio and you attempt to attach to the debuggee with WinDBG you see an error messages nearly worthy of http://www.worsethanfailure.com.
That's a very long way of saying that you've already attached a debugger to the process.
Fortunately for us, WinDBG, as well as its console-based siblings NTSD and CDB, offer a completely different kind of attach; noninvasive. While you young kids reading this don't remember life prior to Windows XP, in the Windows 2000 and prior days once a debugger was attached to a process, it was a life sentence. If you stopped the debugger process, the debuggee was unceremoniously killed. There was no way to detach a debugger like there is now.
What a noninvasive attach does is to call SuspendThread on all the threads in the application. This allows WinDBG's informational commands and extension commands to peer into the process so you can see what's going on with the state of the application. You are not debugging the application so no breakpoints or single stepping works, but you can inspect all sorts of interesting things until you find the problem. To attach noninvasively to a process in WinDBG, select Attach to Process off the File menu (mapped to the F6 key). In the Attach to Process dialog, select you're the process running under the Visual Studio debugger and make sure to check the Noninvasive check box at the bottom of the dialog.
After clicking the OK button WinDBG will report that you're attached noninvasively with text similar to the following:
WARNING: Process 7108 is not attached as a debuggee
The process can be examined but debug events will not be received
Once WinDBG is attached noninvasively to the process, if you switch back to Visual Studio and try to single step or run obviously the action won't happen. Because all the threads are suspended, you'll need to resume the threads by using the WinDBG qd command to quit and detach. With the process back to life, you can proceed to do whatever you want with the Visual Studio debugger.
As I'm constantly attaching and reattaching WinDBG to processes I'm debugging with Visual Studio, I set out to automate doing the attaching. This is especially nice when you're debugging multiple processes as manually attaching different instances of WinDBG to those processes is tedious.
The macro file to automation this process is shown below and downloadable here. It works on both Visual Studio 2005 and Visual Studio 2008. The two macros you can run in AttachWinDBG, AttachToAllProcesses and AttachToCurrentProcess, which are self explanatory. The file you'll download is a Visual Studio Installer file. By double clicking on the .VSI file, it will install the macro into your My Documents\Visual Studio\Projects\VSMacros80 folder. If you have both Visual Studio 2005 and Visual Studio 2008 on the machine, it will install for both development environments. As a .VSI file is just a renamed .ZIP file, if you want to install the .VSMACROS file into a different location, change the extension and extract away.
In the unlikely need that you'll need to uninstall the macro, start Visual Studio, right click on the AttachWinDBG module in Macro Explorer and select Unload Macro Project. Delete the My Documents\Visual Studio\Projects\VSMacros80\AttachWinDBG folder to remove the files.
Before using the macros, you will need to update the full path where you have WinDBG installed in the macro. Open the AttachWinDBG macro in Visual Studio for Application and search for the k_Debugger variable and update the path. If you installed the Debugging Tools for Windows in the default location, you would enter "c:\program files\debugging tools for windows\windbg.exe".
You may also want to change the k_DebuggerCommands variable as well. That string contains the commands you want WinDBG to execute at startup. The string is passed to WinDBG with the –c command line option. My default string loads the SOS and SOSEX extensions and reminds you to use qd to detach.
The only interesting work I had to do in the macro was trying to figure out if you were debugging any remote processes. The Visual Studio automation model does offer any explicit way of determining which machine a process is running on. Luckily I noticed that the Process2.TransportQualifier value was machine name for processes running locally.
As always let me know about any bugs or feature enhancements you'd like to see with AttachWinDBG. Now you have no excuse to making the most out of all the debuggers at your disposal!
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' AttachWinDBG - John Robbins (c) 2007 - john@wintellect.com
' A macro that will attach WinDBG (NTSD/CDB) debugger in noninvasive mode to
' the local processes being debugged by Visual Studio.
'
' See http://www.wintellect.com/jrobbins for more information.
'
' Version 1.0 - December 7, 2007
' - Initial version. Works on both VS 2005 and VS 2008.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Option
Explicit
On
Option
Strict
On
Imports System
Imports EnvDTE
Imports EnvDTE80
Imports System.Diagnostics
Imports System.IO
Imports System.Collections.Generic
Imports System.Text
Imports System.ComponentModel
Public
Module AttachWinDBG
' UPDATE THE FOLLOWING VARIABLE TO POINT TO WHERE THE DEBUGGER YOU WANT
' TO USE FOR THE ATTACH IS LOCATED.
Private
Const k_Debugger As
String = "C:\WinDBG\WinDBG.EXE"
' UPDATE THE FOLLOWING VARIABLE TO CONTAIN THE COMMANDS YOU WANT TO
' EXECUTE ON THE DEBUGGER COMMAND LINE.
Private
Const k_DebuggerCommands As
String = _
".loadby sos mscorwks;.load sosex;.echo **TO DETACH USE THE qd COMMAND**"
' The output pane that all routines have access to.
Private ow As OutputPane = New OutputPane("Attach Debugger")
#Region
"Public Implementation"
Public
Sub AttachToCurrentProcess()
CommonAttachOperation(True)
End
Sub
Public
Sub AttachToAllProcesses()
CommonAttachOperation(False)
End
Sub
#End
Region
#Region
"Private Implementation"
' The common operation function
Private
Sub CommonAttachOperation(ByVal onlyCurrent As
Boolean)
' Clear the output pane.
ow.Clear()
' Always look to see if the k_Debugger value is set up.
If (False = CheckDebuggerExists()) Then
Exit
Sub
End
If
Dim debugger As EnvDTE.Debugger = DTE.Debugger
' If not debugging, there's nothing to do.
If (debugger.CurrentMode = dbgDebugMode.dbgDesignMode) Then
ow.WriteLine("You are not debugging any applications")
Exit
Sub
End
If
' If not stopped, there's nothing to do.
If (debugger.CurrentMode = dbgDebugMode.dbgRunMode) Then
ow.WriteLine("You must break into the debugger before " + _
"using this macro.")
Exit
Sub
End
If
' Build up the list of processes to attach to.
Dim processes As List(Of Process2) = New List(Of Process2)
If (onlyCurrent = True) Then
' Add just the current process
processes.Add(CType(debugger.CurrentProcess, Process2))
Else
For
Each proc As Process2 In debugger.DebuggedProcesses
processes.Add(proc)
Next
End
If
' Strip out all processes being debugged from remote machines.
processes = FilterOutRemoteProcesses(processes)
' If only debugging remote processes, WinDBG can't attach to them.
If (processes.Count = 0) Then
ow.WriteLine("You are not debugging any local processes.")
Exit
Sub
End
If
' Time to loop and attach.
Dim sb As StringBuilder = New StringBuilder()
For
Each proc As Process2 In processes
sb.Length = 0
sb.AppendFormat("-pv -p {0} -c""{1}""", _
proc.ProcessID.ToString(), _
k_DebuggerCommands)
ow.WriteLine("Starting " + k_Debugger + " " + sb.ToString())
Try
System.Diagnostics.Process.Start(k_Debugger, sb.ToString())
Catch ex As Win32Exception
ow.WriteLine("Error: " + ex.Message)
End
Try
Next
End
Sub
' Removes all processes being debugged that are on other machines.
Function FilterOutRemoteProcesses _
(ByVal allProcesses As List(Of Process2)) _
As List(Of Process2)
Dim localProcesses As List(Of Process2) = New List(Of Process2)
Dim computerName As
String = Environment.MachineName
' If the machine name and process transport qualifier are the same,
' we are looking at a local process.
For
Each proc As Process2 In allProcesses
If proc.TransportQualifier = computerName Then
localProcesses.Add(proc)
End
If
Next
Return (localProcesses)
End
Function
' Checks to see if the debugger exists. No point in continuing if it
' does not.
Private
Function CheckDebuggerExists() As
Boolean
Dim exists As
Boolean = File.Exists(k_Debugger)
If (False = exists) Then
ow.WriteLine("The debugger specified: '" + k_Debugger + _
"' does not exist. " + Chr(13) + _
"Please update the k_Debugger " + _
"variable in this macro to the full path to the " + _
"debugger you want to use.")
End
If
Return (exists)
End
Function
#End
Region
End
Module