We added Just-My-Code (JMC) stepping (the ability to step through just your code and skip code that's not yours) in Whidbey. I blogged a demo from the end-user's perspective here. In response to some email, I wanted to talk a little more about how a debugger can implement JMC from the ICorDebug level.
Managed debuggers (VS, MDbg, etc) do step operations (eg: F10, F11, shift+F11) by:
1) creating an ICorDebugStepper object when the debuggee is stopped.
2) setting various flags on the object to describe what type of step operation to do (eg: stop in prologs? skip class-ctors?). This is our attempt to compensate for the many different flavors of stepping.
3) Call the proper Step*() function to tell if it's a step-in, step-over, or step-out.
4) Continue the debuggee.
5) wait for a StepComplete callback for the stepper. Note other debug events may come during the window when the Continue and StepComplete.
It turns out that VS source-level stepping uses several ICorDebugStepper objects under the covers to achieve the desired source-level effect, but that's for another blog entry.
Traditional Stepping Policy:
Steppers have several different flags and options to set to address the different stepping policy decisions.
The "Intercept Mask" tells a stepper if it should stop in certain interceptors like class constructors, security stubs, and exception filters. Use SetInterceptMask([in] CorDebugIntercept mask);
The "Unmapped Stop Mask" tells a stepper how to interact with things that don't map to IL, such as prologs, epilogues, unmanaged code, or unmapped IL regions.Use SetUnmappedStopMask([in] CorDebugUnmappedStop mask). ICorDebugStepper only works in managed code, but the STOP_UNMANAGED flag can tell it to stop when it hits native code (instead of skip through the native code and stop when it comes back into managed code on the other side). This stop option is only allowed when interop-debugging.
There can be various strange conflicts between these two masks, so be prepared. The comments in CorDebug.idl address these enough that I'll avoid further detail here.
What about Just-My-Code?
You can use ICorDebugStepper2::SetJMC(true) to tell a stepper to only stop in functions marked as use code. Functions can be marked as user code by calling ICorDebugModule2::SetJMCStatus(...), ICorDebugFunction2::SetJMCStatus(...), and ICorDebugClass2::SetJMCStatus(). It is entirely the debugger's policy to determine what gets marked as user-code. A debugger may use:
- hints from the project system.
- custom attributes (eg, System.Diagnostics.DebuggerNonUserCode attribute)
- whether it can find symbols for a module
- explicit user input (eg, a "Is JMC" checkbox in a callstack or modules window).
Marking methods as Just-my-code can also affect whether certain MDAs fire (this turned out to be a bad idea), and may also cause additional exception debug events (such as DEBUG_EXCEPTION_USER_FIRST_CHANCE when an exception first enters a function marked as user code). However, the rest of ICorDebug should be agnostic to JMC-status on functions. In particular:
- JMC-status has no affect on callstacks. The debugger can explicitly filter out non-user frames or display them differently if it wishes.
- ICorDebug will still dispatch breakpoints notifications to the debugger for non-user code. The debugger decides how to deal with these (alert the user, silently ignore the breakpoint, etc).
- Calls to Debugger.Break() (VB's "stop" statement) will still generate a Break() debug event for the debugger.
We explicitly wanted to avoid making any sort of JMC-policy decisions at the ICorDebug level so that a debugger could build whatever end-user policy it wants.
Some JMC caveats:
There are some caveats about using JMC.
1) Optimized code can not be marked as user code.
2) A JMC stepper must have a STOP_NONE (0) stopmask. Note that the unmapped stop mask is a mask (eg, bitfield), so if any bits are set or you set STOP_ALL, that that's a non-zero value. STOP_UNMANAGED and STOP_PROLOG are particularly uncooperative with JMC. So if you call SetUnmappedStop(STOP_ALL) and then ICorDebugStepper2::SetJMC(true), the second call will fail (likely with E_INVALIDARG).
3) Be extremely careful if you toggle JMC status with outstanding JMC steppers. This can be a useful technique to lazily initialize JMC status. But it can also cause unpredicted results. You should not toggle JMC status for any code on the stack with outstanding JMC steppers. (We thought about trying to enforce this at runtime, but concluded it would be too expensive in the mainline case).
4) If a function is shared (whether by being domain neutral or by generics), all instances have the same JMC status. (We're not proud of this).
Retrofitting an V1.1 debugger with JMC:
Note that JMC is all new for V2.0. An existing v1.1 debugger can easily add primitive JMC support by:
1) adding some "Enable JMC" mode for end users which under the covers causes the debugger to call ICorDebugStepper2::SetJMC(true) when it creates steppers.
2) adding some policy about when to call SetJMCStatus() on modules, functions, and classes.
That debugger now has basic JMC support. It can add additional sugar and policy on a pay-as-you-go plan.