I’ve had a growing number of people inquire about how to write an interop-debugger with ICorDebug. I strongly advise anybody considering writing an interop-debugger to reconsider for reasons listed here. However, for those who can not be dissuaded, and promise to do something really really cool, here are some key tips:
- Interop-debugging means both the managed + native debugging engines coexist, and your debugger stitches them together to try to make it look like a unified debugging operation.
- Supply an ICorDebugUnmanagedCallback to ICorDebug::SetUnmanagedHandler. This callback is then invoked to dispatch DEBUG_EVENT structures (which are normally retrieved by WaitForDebugEvent in native debugging).
- You’re extremely restricted for what you can do during this callback. Specifically, most of the ICorDebug API can’t be called. In .NET 2.0, we started enforcing this better by checking and returning CORDBG_E_CANT_CALL_ON_THIS_THREAD. Never block in this thread. Just update your state and get out.
- Most importantly, you can’t do an in-band continue on the thread dispatching the ICorDebugUnmanagedCallback. This means the unmanaegd callback thread has to get another thread (perhaps the thread that continues for managed events) to make the in-band continue. There’s actually a good technical reason for this; although it’s not intuitive (witness this forum post)
- If the callback is for an Out-Of-Band event, just update internal state and immediately continue it.
- For Whidbey, use ICorDebugProcess2::SetUnmanagedBreakpoint and ClearUnmanagedBreakpoint instead of setting the raw int3s yourself.
- Don’t call Native debug APIs directly. Instead, call the corresponding API on ICorDebug.
Native Debugging API from kernel32.dll
What to use on ICorDebug
ICorDebug::CreateProcess with DEBUG_ONLY_THIS_PROCESS flag set.
Note that child-process debugging (DEBUG_PROCESS && !DEBUG_ONLY_THIS_PROCESS) is not supported.
ICorDebug::DebugActiveProcess, with win32Attach = true.
Dispatched via the ICorDebugUnmanagedCallback callback, which was set via ICorDebug::SetUnmanagedHandler. Pay close attention to in-band vs. out-of-band events.
Call ICorDebugProcess::ClearCurrentException to get DBG_CONTINUE. Else this acts as DBG_EXCEPTION_NOT_HANDLED
ICorDebugProcess::Set/GetThreadContext. Only use when the thread is in native code and stopped at an inband event.
Use ICorDebugProcess2::SetUnmanagedBreakpoint and ClearUnmanagedBreakpoint instead of writing int3 for native breakpoints.
Use directly, but only when the debuggee is stopped and the thread is in native code.
This is because in order to make interop-debugging work, ICorDebug needs to intercept the native debugging APIs. Note that the native debugging APIs should only be used in native code. Use managed debugging APIs for operations in managed code. Since threads can cross between managed and native, you may need to figure out where the thread is (via a callstack) to determine which operation to use.
- Remember that ICorDebug is for debugging managed code, not native. So the ICorDebug* object hierarchy represents managed constructs, not native ones. For example, native threads don’t get an ICorDebugThread object, and native modules don’t get an ICorDebugModule object. This is why you’ll notice above that all the native-API equivalents are on ICorDebug or ICorDebugProcess. In contrast, since managed constructs are usually built on native ones, you may see native debug objects for managed elements. For example, a managed thread is built on a native thread, so you’ll see a native thread entry for managed threads.
And don’t forget there’s the “Building Development Tools for .Net” forum for further questions.