I mentioned that you can’t have both a managed and native debugger attached to the same process. But I also mentioned that we can’t always enforce that.
So the natural question arises: what if you do attach a managed and native debugger to the same process?
I found out from the compiler lab that some debugger authors want to do exactly that as cheap form of interop-debugging.
There are a few things to be aware of:
1) You need to route the managed debug events. Managed debugging builds execution control on top of native debug events. The CLR has inprocess exception filters to catch these exceptions and convert them into managed debug events (see here for details). A native debugger will get first crack at all exceptions and thus has a chance to steal exceptions from underneath the managed debugger. This applies especially to single-step and breakpoint exceptions. The native debugger needs to make sure it passes these exceptions back to the managed debugger. Failure to do this will hopelessly confuse the managed state and crash the app. In windbg, this is done via ‘gn’ vs. ‘gh’. Unfortunately, the native debugger has no way of knowing for sure if these exceptions belong to the managed debugger. Although it’s probably sufficiently correct to just assume that all single-steps and unrecognized int 3s should be continued not-handled (gn).
2) The native stacktracer will get lost in managed code. The native stacktrace will not recognized jitted managed code and thus likely get lost. Practically this means that the native stackwalker will be able to see the native leafmost portion of each stack up until when that stack enters managed code. It will likely not be able to see native portions on the stack that are sandwiched between managed code.
3) The managed debugger will hang when the native debugger is stopped. Since the native debugger is in hard-mode, it will stop all threads in the debuggee including the helper-thread, which will thus hang the managed debugger until the native debugger resumes. This means that if you stop in the native debugger at a breakpoint, you won’t be able to use the managed debugger to view any managed state.
4) You will not get an integrated managed + native debugging story. Because the managed and native debugging services won’t be cooperating, the separate states won’t be merged. For example, you won’t see a mixed-mode callstack containing both managed and native code. You also won’t be able to step from managed to native code (or vice-versa) as a single atomic operation. You also won’t be able to inspect managed state from the native world, which can be significant for inspecting certain MC++ datatypes that place native data in opaque managed blobs.
I’ve had people ask me which method I’d recommend to get both managed+native debugging support: 1) Interop-debugging or 2) attaching simultaneous debuggers.
That’s a toughy. Interop-debugging provides a clean and consistent answer to all of these issues; the drawback is that it’s significantly more complicated for a debugger author to implement. In v1.0, I think we actively scared debugger authors away from implementing interop-debugging. In v2.0, we’ve made interop-debugging significantly more stable and perfomant. But it’s still a very complicated interface to use, and the documentation is very lacking.
I’m toying with the idea of writing an MDbg extension for basic interop-debugging support. If I can do that, I’ll certainly post about it here. That would be a good green light for other debugger authors to try and add interop-debugging to their own tools.