Bug Bash

Let The CLR Find Bugs For You With Managed Debugging Assistants

Stephen Toub

This article discusses:
  • What MDAs are and their usefulness in debugging
  • Common scenarios in which MDAs are beneficial
  • Enabling MDAs for managed and unmanaged debugging
This article uses the following technologies:
.NET Framework, Visual Studio 2005

Contents

PInvokeStackImbalance
LoadFromContext
CallbackOnCollectedDelegate
GcUnmanagedToManaged
InvalidFunctionPointerInDelegate
AsynchronousThreadAbort
StreamWriterBufferedDataLost
PInvokeLog
Enabling MDAs
Conclusion

A sserts are a very valuable developer tool that allow you to test for specific conditions at run time. Often, you"re testing for situations that should never occur ("if this happens, something is wrong"), and sometimes you"re looking to verify that particular conditions do occur ("if this doesn"t happen, something is wrong"). Since these asserts aren"t meant for users" eyes, typically they"re only compiled into debug builds, or a log file is generated that maintains information about a series of such problems.

That"s all well and good for application developers, but it"s not great for developers of libraries. Developers of apps that consume libraries can benefit from the libraries providing these kinds of alerts, and yet frequently such functionality is intrusive. Instead of asserts, library vendors typically opt to throw exceptions which can bubble up to the application level to be dealt with. But exceptions aren"t appropriate substitutes for asserts in all cases.

For example, consider how the common language runtime (CLR) loads assemblies. There are two "contexts" into which an assembly can be loaded: the Load context and the LoadFrom context. Use of the LoadFrom context is frequently the cause of bugs and unexpected behavior related to serialization, casting, and assembly dependency resolution. You wouldn"t want an exception to be thrown any time an assembly is loaded into the LoadFrom context, but you might like to receive some sort of debug-time notification that the condition is occurring, and you"d like to be able to turn such notifications on and off easily. That would allow you to look into the situation and determine whether it"s a legitimate usage or an unintended side effect of some code on which you"re dependent. Enter Managed Debugging Assistants (MDA).

MDAs are asserts (probes) in the CLR and in the base class libraries (BCL) that can be turned on or off. When enabled, they provide information on the CLR"s current runtime state and on events that you as a developer could not otherwise access. Some even modify runtime behaviors to help expose otherwise very hard to find bugs. The Microsoft® .NET Framework 2.0 includes 42 of these MDAs, some more useful than others, but all of them very powerful debugging aids that can help you track down truly insidious problems at run time.

The MDAs currently available fall into three categories, as shown in the Type column in Figure 1. Informational MDAs provide data about the current state of the CLR and are not enabled by default. Behavioral MDAs are also not enabled by default. But rather than simply providing information to the developer, these MDAs modify the behavior of the CLR in an attempt to highlight particular bugs in the developer"s code. Detection MDAs directly identify some type of error condition that is occuring. Depending on the environment, some detection MDAs are easily enabled. As shown in Figure 2, the detection MDAs in the Managed Debugger category are available for selection from the Exceptions dialog in Visual Studio® 2005. A subset of the Managed Debugger category is checked by default in the Exceptions dialog and as a result will cause Visual Studio 2005 to halt debugging with an Exception Assistant dialog when encountered. When running under an unmanaged debugger, only two detection MDAs are enabled by default.

Figure 1 MDAs in the .NET Framework 2.0

  Type Managed Debugger Unmanaged Debugger
Unmanaged Interop      
Reentrancy Detection (on by default)  
GcManagedToUnmanaged Behavioral    
GcUnmanagedToManaged Behavioral    
Unmanaged Interop - COM      
ContextSwitchDeadlock Detection (on by default)  
DisconnectedContext Detection (on by default)  
EarlyBoundCallOnAutoDispatchClassInterface Detection    
ExceptionSwallowedOnCallFromCom Detection    
FailedQI Detection    
InvalidApartmentStateChange Detection    
InvalidIUnknown Detection    
InvalidMemberDeclaration Detection (on by default)  
InvalidVariant Detection (on by default)  
MarshalCleanupError Detection    
Marshaling Informational    
NonComVisibleBaseClass Detection (on by default)  
NotMarshalable Detection    
RaceOnRCWCleanup Detection (on by default)  
ReportAvOnComRelease Detection    
Unmanaged Interop - P/Invoke      
CallbackOnCollectedDelegate Detection (on by default)  
InvalidFunctionPointerInDelegate Detection (on by default)  
InvalidOverlappedToPinvoke Detection    
OverlappedFreeError Detection    
PInvokeLog Informational    
PInvokeStackImbalance Detection (on by default)  
Loader      
BindingFailure Detection    
DllMainReturnsFalse Detection    
LoaderLock Detection (on by default)  
LoadFromContext Detection    
Constrained Execution Regions      
IllegalPrepareConstrainedRegion Detection    
InvalidCERCall Detection    
OpenGenericCERCall Informational    
VirtualCERCall Informational    
Threading      
AsynchronousThreadAbort Detection    
DangerousThreadingAPI Detection    
Base Class Libraries      
DateTimeInvalidLocalFormat Detection (on by default)  
MemberInfoCacheCreation Informational    
ModuloObjectHashcode Behavioral    
ReleaseHandleFailed Detection    
StreamWriterBufferedDataLost Detection    
Miscellaneous      
FatalExecutionEngineError Detection (on by default)  
InvalidGCHandleCookie Detection    
JitCompilationStart Informational    

Figure 2 MDAs in the .NET Framework 2.0

Figure 2** MDAs in the .NET Framework 2.0 **

You might find certain MDAs more useful than others. My opinion is that you should start your development with all of the MDAs in the Managed Debugger category turned on in the Exceptions dialog, only disabling ones when you actively determine that they"re getting in the way. Especially when doing any sort of interop work, these MDAs can help bring to light problems that might otherwise go unnoticed for quite some time. In fact, as Mike Stall points out in his blog, the inclusion of MDAs in the .NET Framework 2.0 was, in a very real sense, a self-serving act by members of the .NET Framework team. Many problems can be caught by MDAs that would otherwise manifest as apparent bugs with the Framework, even though the symptom may be just one in a series of dominoes knocked over by something wrong much earlier in the developer"s code.

I do have a few favorite MDAs, which I"ll describe next.

PInvokeStackImbalance

If you do any P/Invoke work, this MDA is a must. It"s activated when the CLR detects that the stack depth after a P/Invoke call does not match the expected stack depth. Consider a simple method exported from kernel32.dll for which you might write an interop signature:

BOOL Beep(DWORD dwFreq, DWORD dwDuration);

A valid P/Invoke declaration could look like this:

[DllImport("kernel32.dll", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Beep(int frequency, int duration);

But what if you accidentally, due to haste or to a misunderstanding of how native and managed types relate, declare it as follows:

[DllImport("kernel32.dll", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Beep(long frequency, long duration);

By mistaking the parameters as longs instead of as ints, the caller will be passing into the target method 8 bytes more data than it actually expects (two 64-bit values instead of two 32-bit values). The Beep function utilizes the StdCall calling convention, which means that the callee is responsible for removing the parameters from the stack. Thus, the callee will remove 8 bytes from the stack (for the two 32-bit values it expected), leaving the stack unbalanced on return (the additional 8 bytes erroneously supplied). When this function returns to the CLR, the PInvokeStackImbalance MDA will be raised, resulting in a dialog like that shown in Figure 3.

Figure 3 PInvokeStackImbalance MDA Activation

Figure 3** PInvokeStackImbalance MDA Activation **

Of course, the world of PInvokeStackImbalance isn"t entirely sunny. The just-in-time compiler (JIT) in the CLR implements several approaches to invoking native functions through P/Invoke, ranging from a very fast inlining technique down to a slow interpretation technique. The JIT chooses which technique to use based on a variety of factors, including the complexity of the P/Invoke declaration. Unfortunately, when the PInvokeStackImbalance MDA is enabled, P/Invoke calls are forced to use the slower techniques. In typical C# and Visual Basic®-based apps, P/Invoke is used relatively infrequently, so the performance hit is outweighed by the value provided by the MDA. However, applications that do a significant amount of interop (such as those written in C++ to take advantage of It Just Works interop) could experience noticeable performance degradation when run under the debugger with this MDA enabled. See Mike Stalls blog.

LoadFromContext

The scenario I described earlier for tracking assemblies loaded into the LoadFrom context was not hypothetical. The LoadFromContext MDA will alert you to any time any assembly is loaded into the LoadFrom context, making it possible to more easily identify bugs that manifest as invalid cast exceptions, deserialization failures, and the like. If at all possible, it"s best to install assemblies in the Global Assembly Cache or in the ApplicationBase directory, which then allows you to use Assembly.Load when explicitly loading assemblies.

CallbackOnCollectedDelegate

Frequently, interop work requires Windows® calling back into managed code, a process known as reverse P/Invoke. This is typically accomplished by passing a delegate to a P/Invoke method. Under the covers, the CLR creates a native function pointer to a thunk that, when invoked, in turn invokes the delegate. In the unmanaged world, that function pointer is a constant, and the unmanaged code that uses it rightfully expects the target to always exist. However, when the delegate is no longer needed by managed code, the garbage collector is free to reclaim it (reclaiming the delegate causes the associated thunk to be freed as well), which will result in the function pointer in unmanaged code becoming invalid. When the unmanaged code then attempts to call this invalid function pointer, access violations will typically ensue. Not good.

To prevent this, the delegate must be kept alive for as long as the unmanaged code might need to call it. Some of this work is done for you. For example, take the EnumWindows function exposed from kernel32.dll. EnumWindows accepts a callback function as a parameter, and that function will be called for each top-level window found. All of these callbacks happen during the invocation of EnumWindows. It is for situations like this that the CLR ensures that a delegate passed to a P/Invoke method will not be collected during the invocation of that method. But what if the unmanaged code caches away a copy of the function pointer supplied to it? At a later point in time, after the managed P/Invoke call finishes, the unmanaged code might attempt to call back on that function pointer, at which point the delegate might not be alive.

For a concrete example of this, consider the (buggy) code in Figure 4. Using a low-level keyboard windows hook, the code intercepts all WM_KEYDOWN messages sent to any window and prints out the corresponding key. When you run this app, it"ll most likely appear to work, at least for a little while. But at some point, the garbage collector will run and will see that the LowLevelKeyboardProc delegate instance passed to the SetWindowsHookEx P/Invoke method is no longer referenced anywhere, and the GC will reclaim it (if you don"t feel like waiting, insert a call to GC.Collect just before the call to Application.Run to force the delegate"s immediate removal). After that point, the next time Windows attempts to use this hook, the app will almost certainly die a horrible death, and it might not be at all apparent why.

Figure 4 Buggy Code for Setting a Low-Level Keyboard Hook

using System; 
using System.Diagnostics;
using System.Windows.Forms;
using System.Runtime.InteropServices;

public class InterceptKeys
{
    private const int WH_KEYBOARD_LL = 13;
    private const int WM_KEYDOWN = 0x0100;
    private static IntPtr _hookID = IntPtr.Zero;

    public static void Main()
    {
       _hookID = SetHook();
       Application.Run();
       UnhookWindowsHookEx(_hookID);
    }

    private static IntPtr SetHook()
    {
        using (Process curProcess = Process.GetCurrentProcess())
        using (ProcessModule curModule = curProcess.MainModule)
        {
            return SetWindowsHookEx(WH_KEYBOARD_LL, 
                new LowLevelKeyboardProc(HookCallback), // BUG!!!
                GetModuleHandle(curModule.ModuleName), 0);
        }
    }

    private delegate IntPtr LowLevelKeyboardProc(
        int nCode, IntPtr wParam, IntPtr lParam);

    private static IntPtr HookCallback(
        int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            int vkCode = Marshal.ReadInt32(lParam);
            Console.WriteLine((Keys)vkCode);
        }
        return CallNextHookEx(_hookID, nCode, wParam, lParam);
    }

    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook, 
        LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError = true)]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, 
        IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetModuleHandle(string lpModuleName);
}

CallbackOnCollectedDelegate to the rescue! When this MDA is enabled, thunks are not deleted when the delegate is garbage collected. Instead, the thunk is modified to activate the CallbackOnCollectedDelegate MDA so that you know exactly why your process is about to die (see Figure 5). Note that the CLR only keeps around a certain number of these modified thunks, and that number is configurable using an MDA configuration file (more on these shortly), though it"s rare you"d ever need or want to change this number.

Figure 5 PInvokeStackImbalance MDA Activation

Figure 5** PInvokeStackImbalance MDA Activation **

After CallbackOnCollectedDelegate helps identify the problem, you need to fix it. Typical solutions are to use GC.KeepAlive to ensure a particular instance is kept alive until a certain time, or to make it a member variable of a class that itself will stay alive for a long enough period of time.

GcUnmanagedToManaged

In my discussion of CallbackOnCollectedDelegate, I mentioned that you could easily force the error by calling GC.Collect after setting up the hook (by this point all references to the delegate were gone). That"s certainly one solution, but it also requires modifications to your code, and one of the best benefits of MDAs is debugging help and error detection without code modification. This is where the GcUnmanagedToManaged MDA comes into play. Falling into the behavioral category, this MDA does not produce any output; instead, it causes the GC to perform a collection any time a thread transitions from unmanaged code to managed code. Thus, with this MDA enabled in addition to CallbackOnCollectedDelegate, any time the unmanaged code attempts to invoke the delegate, garbage collection will first be performed. This can come at a significant performance cost if there are lots of callbacks (resulting in lots of additional collections), but it also can significantly decrease the time between when the corruption happens (in this case, dropping all references to the delegate) and when the CallbackOnCollectedDelegate MDA alerts you to a problem. All that, with no modifications to your code, is a win in my book.

InvalidFunctionPointerInDelegate

A frequently asked for feature in the .NET Framework 1.x was the ability to make P/Invoke calls to dynamic targets, because the name, signature, or location of the target function was not known at compile time. This functionality is enabled in the .NET Framework 2.0 through a new method on the Marshal class, GetDelegateForFunctionPointer. It accepts two parameters: an IntPtr address of the target function and the Type of the delegate to instantiate around that function (the delegate"s signature should be compatible with the target function).

What happens if you pass a bad function pointer to Marshal.GetDelegateForFunctionPointer? It"ll happily create the delegate for you, but when you invoke that delegate, you could very well find yourself starting at an access violation, or worse. To help you find such problems, the InvalidFunctionPointerInDelegate MDA alerts you when an invalid function pointer is passed to GetDelegateForFunctionPointer. Consider the following code:

EventHandler ev = Marshal.GetDelegateForFunctionPointer(
    (IntPtr)0x12345678, typeof(EventHandler));
ev(null, EventArgs.Empty);

When run, an Exception Assistant alerts you at the first line with the following message:

Invalid function pointer 0x12345678 was passed into
the runtime to be converted to a delegate. 
Passing in invalid function pointers to be converted
to delegates can cause crashes, corruption or data loss.

To top it all off, as shown in Figure 1, this MDA is enabled by default in Visual Studio, so you don"t even have to turn it on to take advantage of it. Automatic protection.

AsynchronousThreadAbort

Regular readers of the .NET Matters column may remember several previous columns in which I demonstrated how to use the Thread.Abort method. In one edition it was used to implement method timeouts (See ".NET Matters: StringStream, Methods with Timeouts"), while in another it helped to implement a wrapper for the ThreadPool that supports cancellation (See ".NET Matters: Abortable Thread Pool"). In both of those columns I elaborate on the dangers of using this technique, and in my article on reliability in the .NET Framework (See "High Availability: Keep Your Code Running with the Reliability Features of the .NET Framework"), I take a detailed look at the problems it can cause and on a variety of ways to improve the reliability of your managed applications.

In short, unless Thread.Abort is being used to abort the current thread (Thread.CurrentThread.Abort), avoid it whenever possible. To help alert you to situations in which your code is making use of Thread.Abort, the CLR provides the AsynchronousThreadAbort MDA. Like the LoadFromContext MDA, the AsynchronousThreadAbort MDA does not definitively indicate you have a problem in your code, but it does help you root out the causes of problems you might be experiencing by letting you know that you"re using something dangerous. Consider the following simple program:

class Program {
    public static void Main(string[] args) {
        Thread t = new Thread(delegate() {
            while (true) { Thread.Sleep(500); }
        });
        t.Start();
        t.Abort();
        Console.WriteLine("Done");
    }
}

This code spins up a new thread and immediately aborts it. If the AsynchronousThreadAbort MDA is enabled (a simple task, as it"s part of the Managed Debugger category and thus can be enabled from the Exceptions dialog in Visual Studio), Visual Studio will break at the line that aborts the thread, providing an Exception Assistant with the following text (though almost certainly with different thread IDs):

User code running on thread 4408 has attempted to abort thread 4956.
This may result in corrupt state or resource leaks if the thread being
aborted was in the middle of an operation that modifies global state
or uses native resources. Aborting threads other than the currently running
thread is strongly discouraged.

The MDA tells you exactly which thread was being interrupted and which thread was doing the interrupting, a very helpful feature when you, through the debugger, have access to all threads involved in the operation.

StreamWriterBufferedDataLost

Another gem of an MDA, StreamWriterBufferedDataLost alerts you to data loss that can result by not properly closing a StreamWriter. File I/O is expensive, so to minimize the number of I/O operations, StreamWriter buffers its data. When you call StreamWriter.Write, the data you pass may or may not be written immediately to the underlying file, depending on how much data you"re writing and how much you"ve already written. At some point, StreamWriter decides it"s received enough data and that it should be written to the file, at which time it does the actual I/O. Until then, it stores the data to be written in an internal buffer. If you forget to close the StreamWriter, the StreamWriter might contain some unflushed data in its buffers, and that data will never make it into the output file. If the StreamWriterBufferedDataLost MDA is enabled, the StreamWriter"s finalizer will activate the MDA if it detects unflushed data in its buffer, at which point the Exception Assistant will display information like that shown in Figure 6. Notice that the MDA not only tells you that the situation occurred, but it also gives you a full stack trace for where the StreamWriter whose data was abandoned was instantiated. How cool is that?

Figure 6 Message from StreamWriterBufferedDataLost MDA

A StreamWriter was not closed and all buffered data 
within that StreamWriter was not flushed to the underlying stream.
(This was detected when the StreamWriter was finalized with data 
in its buffer.)  A portion of the data was lost.  Consider one of
calling Close(), Flush(), setting the StreamWriter’s AutoFlush property 
to true, or allocating the StreamWriter with a "using" statement.  Stream type: System.IO.FileStream
File name: C:\test.txt
Allocated from:
   at System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   at System.IO.StreamWriter.Init(Stream stream, Encoding encoding, Int32 bufferSize)
   at System.IO.StreamWriter..ctor(String path, Boolean append, Encoding encoding, Int32 bufferSize)
   at System.IO.StreamWriter..ctor(String path)
   at ConsoleApplication10.Program.Main(String[] args)
   at System.AppDomain.nExecuteAssembly(Assembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext,
   ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

PInvokeLog

Ever curious about what P/Invoke calls are being made by your application? When enabled, the PInvokeLog MDA fires when each P/Invoke declaration is invoked for the first time, allowing you to see exactly which unmanaged function is executed and which managed P/Invoke signature caused that to happen. Unfortunately, not all of that information is accessible to you in Visual Studio. Up until this point, I"ve glossed over how MDAs actually report information to a debugger. For the .NET Framework 2.0, the ICorDebug interfaces have been extended to support MDAs, specifically through ICorDebugManagedCallback2 and its MDANotification method. MDANotification provides the listener with an ICorDebugMDA instance, providing valuable information about the raised MDA, including an XML description. For CallbackOnCollectedDelegate, that XML output will look something like this:

<mda:msg xmlns:mda="https://schemas.microsoft.com/CLR/2004/10/mda">
  <!-- 
     A callback was made on a garbage collected delegate of type
     "test!InterceptKeys+LowLevelKeyboardProc::Invoke". This may 
     cause application crashes, corruption and data loss. When 
     passing delegates to unmanaged code, they must be kept alive 
     by the managed application until it is guaranteed that
     they will never be called.
   -->
  <mda:callbackOnCollectedDelegateMsg break="true">
    <delegate name="test!InterceptKeys+LowLevelKeyboardProc::Invoke"/>
  </mda:callbackOnCollectedDelegateMsg>
</mda:msg>

If you compare this output with that from Figure 5, you"ll see that Visual Studio is displaying in the Exception Assistant the inner text comment from the MDA"s output. For PInvokeLog, the XML output from the MDA looks something like this:

<mda:msg xmlns:mda="https://schemas.microsoft.com/CLR/2004/10/mda">
  <mda:pInvokeLogMsg>
    <method name="test!InterceptKeys::CallNextHookEx"/>
    <dllImport dllName="C:\WINDOWS\system32\USER32.dll" 
        entryPoint="CallNextHookEx"/>
  </mda:pInvokeLogMsg>
</mda:msg>

PInvokeLog will typically fire many times for even the simplest of .NET applications, and this sample is the output generated when the InterceptKeys.CallNextHookEx P/Invoke from Figure 4 was called. Unfortunately, Visual Studio doesn"t display the entire XML output, only the inner text as shown in the previous example. If you want all of this information, you"ll need to use a debugger that displays the full XML, such as Windbg. Of course, Windbg doesn"t have an Exceptions dialog as does Visual Studio, so you"ll need to enable the PInvokeLog MDA in a slightly different fashion.

Enabling MDAs

There are several ways to enable MDAs. If the MDA is in the Managed Debugger category (see Figure 1) and if you"re using Visual Studio, the Exceptions dialog is the way to go. However, this only covers a subset of the cases. MDAs can be enabled in two additional ways: through the registry and through an environment variable, both of which can be done in combination with a configuration file. Enabling MDAs through the registry involves setting the MDA value on the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework key to 1, as you can do with a .reg file containing the following:

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework]
"MDA"="1"

This tells the CLR to look for an MDA configuration file in the same directory as the application with the name AppName.exe.mda.config, where AppName.exe is the name of the application being debugged. The XML contents of this file lists all of the MDAs you want enabled for this application. So, for example, a configuration file that enabled the eight MDAs I"ve described up to this point would look like the following (note that the XML is case-sensitive with respect to the names of the MDAs, and the MDAs must be listed in alphabetical order).

<?xml version="1.0" encoding="UTF-8" ?>
<mdaConfig>
  <assistants>
    <asynchronousThreadAbort />
    <callbackOnCollectedDelegate />
    <gcUnmanagedToManaged />
    <invalidFunctionPointerInDelegate />
    <loadFromContext />
    <pInvokeLog />
    <pInvokeStackImbalance />
    <streamWriterBufferedDataLost />
  </assistants>
</mdaConfig>

Some MDAs take additional parameters (you can find these parameters documented in the MSDN library), and in fact some must be configured in order to be worth anything. As an example of an MDA that can be configured but that doesn"t have to be, consider a configuration entry for the CallbackOnCollectedDelegate MDA that was discussed earlier:

    <mdaConfig>
      <assistants>
        <callbackOnCollectedDelegate listSize="1500" />
      </assistants>
    </mdaConfig>

The listSize attribute on the callbackOnCollectedDelegate element tells the CLR how many abandoned interop thunks to retain for MDA purposes, in this case 1500 (the value must be at least 50 and no more than 2000; the default is 1000). PInvokeLog, on the other hand, must be configured with additional information in order to be at all useful. PInvokeLog needs to be told which target DLLs should have output provided:

<mdaConfig>
  <assistants>
    <pInvokeLog>
      <filter>
        <match dllName="user32.dll"/>
        <match dllName="kernel32.dll"/>
      </filter>
    </pInvokeLog>
  </assistants>
</mdaConfig>

Enabling MDAs in the registry is global and will affect all managed applications you run. Instead, for many situations you"ll probably find it easier to enable MDAs using an environment variable. Like the registry key, the COMPLUS_MDA environment variable can be set to 1 to force executed applications to use an associated configuration file:

set COMPLUS_MDA=1

However, unless you need to configure specific MDAs with specific parameters, you can avoid a configuration file and instead set the environment variable to a semicolon-delimited list of the MDAs you want enabled. For example, the following command will enable just the GCUnmanagedToManaged and CallbackOnCollectedDelegate MDAs:

set COMPLUS_MDA=GCUnmanagedToManaged;CallbackOnCollectedDelegate

After setting this variable, from the command line you can compile and execute the code in Figure 4:

    csc /debug /o- BuggyCode.cs
    BuggyCode.exe

As soon as BuggyCode.exe intercepts your next key press, you"re presented with the standard debugger attach dialog (see Figure 7), stemming from the CallbackOnCollectedDelegate MDA"s invocation.

Figure 3 Just-In-Time Debugging for an MDA

Figure 3** Just-In-Time Debugging for an MDA **

You can now use Windbg to view the output from PInvokeLog. Again, using the example in Figure 4 along with the PInvokeLog configuration file I showed earlier (which should be named BuggyCode.exe.mda.config and be in the same directory as BuggyCode.exe), you can execute it as follows:

    set COMPLUS_MDA=1
    Windbg BuggyCode.exe

As soon as Windbg loads and you continue execution, you"ll see the output shown in Figure 8, containing a plethora of XML snippets from the PInvokeLog MDA.

Figure 8 Using WinDbg to View PInvokeLog Output

Figure 8** Using WinDbg to View PInvokeLog Output **

If you want to use Visual Studio with MDAs like PInvokeLog, you can, but it requires a bit of extra work, and while you"ll be notified when the MDA fires, you won"t get any of the additional information provided about the MDA (such as the DLL import information in the case of PInvokeLog). First go to the Exceptions dialog in Visual Studio and add an entry under Managed Debugging Assistants with the name of the MDA in which you"re interested. Make sure you"ve enabled MDAs in the registry and that you"ve properly created a configuration file that includes the MDA in which you"re interested. Additionally, some MDAs (like PInvokeLog) will only fire in Visual Studio when mixed-mode debugging is enabled. After taking all of those steps and executing my program, every time the PInvokeLog MDA fires, you"ll see a dialog like that in Figure 9.

Figure 9 PInvokeLog MDA in Visual Studio

Figure 9** PInvokeLog MDA in Visual Studio **

Conclusion

A common question regarding MDAs is whether you as a developer can add your own. Unfortunately, no, you can"t, at least not with the .NET Framework 2.0. It"s possible future versions of the Framework will allow for this capability, but for now, it"s great to have the level of support offered thus far. Give MDAs a look; I think you"ll be happy with what you see.

Stephen Toub is the Technical Editor for MSDN Magazine.