How can I debug Just My Code?

Sometimes developers want to debug just the code they wrote and not the 3rd-party code (such as framework and libraries) that’s also inside their app. This becomes particularly useful when user and non-user code call back and forth between each other.  The v2.0 CLR debugging services have a host of new features to support this, which we call “Just-My-Code” (JMC) debugging.

 

In V2.0, ICorDebug:

- lets a debugger mark each function as either user or non-user code. It’s up to the debugger to determine what is and is not user code. Visual Studio will use hints from the project system and also assume if that a given module is non-user code if the symbols are missing. You can also use the System.Diagnostics.DebuggerNonUserCodeAttribute attribute to tell VS to mark a specific method as non-user code.

- allows stepping operations to magically skip all non-user code.

- provides additional exception notifications.

 

A debugger can also do additional things such as filtering non-user code from the callstack.

In this blog, I’ll demo JMC stepping and explain how to use this new functionality from ICorDebug. (I’ll blog about exceptions later)

 

A very simple example of JMC-stepping:

Here’s a trivial example of JMC-stepping. Run this as a console application in VS2005 beta1.

using System;

class Program

{

    static void Main()

    {

        NonUserLibraryCode(); // <-- step in here (F11 in Visual Studio)

    }

    // This attribute tells the debugger to mark this function as non-user code.

    [System.Diagnostics.DebuggerNonUserCode]

    static void NonUserLibraryCode()

    {

        Console.WriteLine("Before");

        UserCode();

        Console.WriteLine("After");

    }

   

    static void UserCode()

    {

        Console.WriteLine("User1"); // <-- step completes here, skipping the non-user code

    }

}

 

VS also can filter out the non-user code from the callstack. So when stopped inside UserCode(), the callstack looks like:

>          ConsoleApplication2.exe!Program.UserCode() Line 21            C#

            [External Code]           

            ConsoleApplication2.exe!Program.Main() Line 7 + 0x6 bytes   C#

            [External Code]           

In VS, JMC can be disabled from Tools | Options | Debugging, “Enable Just My Code” check box (it must be enabled for these examples to work). The callstack filtering can be toggled by right clicking the callstack to toggle “Show External Code”.

All of MDbg’s JMC support is only in extension dlls that we use for testing purposes.

 

A more real example:

The example above is very simple because everything can be determined statically. This example shows Main dynamically invoking callbacks. Some callbacks are user code and others aren’t. This is similar to the winforms case where the message loop is non-user code but some of the handlers (like button1_click) are user code. You can use JMC to step between your handlers without having to put breakpoints in each handler and without having to step through the owning message loop.

 

using System;

using System.Diagnostics;

class Program

{

    delegate void Callback();

    // This will invoke 3 callbacks. A user, non-user, and then a user one.

    // (1) Step-in here. Since Main() is non-user code, we skip straight to the first bit of user

    // code called which is UserCodeHandler1.

    [DebuggerNonUserCode]

    static void Main()

    {

        // Invoke some callbacks.

        Callback[] list = new Callback[] {

            new Callback(UserCodeHandler1), new Callback(NonUserCodeHandler), new Callback(UserCodeHandler2)

        };

        foreach (Callback fp in list)

        {

            fp();

        }

    }

   

    static void UserCodeHandler1()

    {

        Console.WriteLine("Inside my 1st handler!"); // <-- step completes here from (1)

        // (2) Do a step-in here. Normally, a step-in at the end of a method would be a step-out.

        // But since our caller (Main) is non-user code, we don't want to stop there. So we'll run to the next bit of user code.

    }

    [DebuggerNonUserCode]

    static void NonUserCodeHandler()

    {

        Console.WriteLine("Inside a non-user code handler");

    }

    static void UserCodeHandler2()

    {

        // (3) Step-in from (2) lands here.

        Console.WriteLine("Inside my 2nd handler!"); // <-- step completes here, skipping the non-user code

    }

}

 

Why JMC is cool on a technical level.

JMC-stepping has some good technical accomplishments in it. It is …

1) … not limited to static anaylsis (as shown above). JMC works with everything including arbitrarily deep callstacks, multicast delegates, polymorphism / virtual functions, and events + callbacks. There are no smoke and mirrors here - you could construct arbitrarily complicated examples.

2) ... very performant. The step operation may skip large amount of non-user code without a performance penalty.  Constrast this to trying to fake JMC by just using traditional single-step operations to skip through all non-user code (which would be unusably slow).

3) … thread-safe. You can use JMC-stepping in multi-threaded programs without any problems. (Some other solutions would break down here)

4) … very configurable. You can set JMC-status on a per-function level, and you can toggle that status at runtime.

 

JMC-stepping on the ICorDebug level:

ICorDebug implements managed stepping via ICorDebugStepper objects. All the low-level details of how managed stepping actually works are abstracted away from the debugger.

Debuggers create a JMC-stepper by calling ICorDebugStepper2::SetJMC(true). A JMC-stepper will magically skip all non-user code. All non-JMC steppers (which I’ll call “Traditional Steppers”) ignore JMC-status. Thus a debugger must explicitly request JMC else it will get the pre-JMC behavior. This allows JMC functionality to be non-breaking.

 

The CLR defaults to assuming everything is non-user code and defers all JMC decision making to the debugger via the ICorDebug API. Debuggers must mark user code by calling ICorDebug(Function|Class|Module)::SetJMCStatus(). Debuggers can use any heuristics they wish to decide what is and is not user code. So although the DebuggerNonUserCode attribute is defined in mscorlib, the CLR does not pay any attention to it. That attribute is there exclusively to provide a convenient primitive semi-standard protocol for debuggers to mark specific functions as non-user code.  A debugger could use other heuristics such as:

- input from the project system / IDE

- method names (eg, make all ToString() or property getters non-user code).

- other custom attributes. Eg, a custom attribute could take a string parameters and the debugger could use that to create groups of JMC methods.

- presence of symbols. (generally modules without symbols should be considered non-user code).

- source-level information (since the debugger has access to the source).

 

JMC-status can be toggled at runtime. Thus a debugger could build fancy logic to toggle groups of functions at runtime. This also lets a debugger delay setting JMC status until the user first stops in that method.

 

At the ICorDebug level, JMC is orthogonal to breakpoints. Breakpoints will be hit in non-user code, but a debugger may choose to continue the debuggee anyways and not notify the user. (This is what VS does).