Fake attach event ordering

When you attach to a managed debuggee (via ICorDebug::DebugActiveProcess), ICorDebug generates a set of fake events designed to bring the debugger up to the current state. The motivation is that it pumps the debugger just as if the debugger was always attached. Native debugging does the same thing.

The list isn't very well documented, and I've had enough requests to clarify it. This is what you would observe in .NET 1.0, 1.1 and 2.0. I've constructed this mostly from memory (with some double-checking); and the order may change in the future.

Between each event, you must call Continue just as with real live events. The fake events are also split across several callbacks queues, which are noted via <end callback queue> items in the list. The exact partitioning of callback queues is something that could easily change, so robust debuggers shouldn't rely on them.

Here's the list of fake attach events: (update:1/17/07, forgot UpdateModuleSyms)

  1. a CreateProcess event. This doesn't come until the process has actually loaded managed code.
  2. a CreateAppDomain event for each AppDomain.  Debuggers should call ICorDebugAppDomain::Attach for each appdomain, especially when working with .NET 1.X.
  3. <end callback queue>
  4. a LoadAssembly event for each Assembly in each appdomain. These are generally not very interesting.  These come in the order the assemblies are loaded.
  5. a LoadModule for each module in each assembly.
    - This is an opportunity to request class load events. I recommend not requesting the events (which is the default).
    - These come in the order the modules are created. Visual Studio exposes this ordering via one of the columns in the modules window.
    - The order here is also important because of metadata dependencies. For example, in a multi-module assembly, the manifest module should come first (this is especially important in reflection.emit scenarios).
  6. a UpdateModuleSymbols eventif the runtime has symbols for the module. This is most common for a module generated with Ref.emit with symbols. It could also include an in-memory module where the host has the symbols. This delivers an IStream containing the symbols that then be used with the symbol reader.
  7. <end callback queue>
  8. a LoadClass for each class in each module. These are not particularly useful because they don't really mean anything.
  9. <end callback queue>
  10. CreateThread for each managed thread.
    - These come in the order that the threads were created. This means that the first CreateThread event is for the main thread of the app.
    - Depending on whether the finalizer has executed managed code yet, the total number of events here may appear random. 
  11. A CreateConnection and ChangeConnection event for each connection.
    - Connections are new in .NET 2 and only occur in certain Hosted scenarios.
    - Your debugger will very likely never see them; but I want to mention them for completeness sake.
  12. In case of a managed just-in-time (JIT) attach (where the debuggee initiates the attach), the event that triggered that attach is dispatched.
    - This is most likely an Unhandled Exception, User Break, or MDA event. 
    - These events are generally shell stopping events. This is why a debugger would naturally stop at a jit-attach; but keep running at a normal attach.
  13. <end callback queue>

Attach is now complete.  
 

Here are some more caveats:

  1. Note that the ordering is breadth first, not depth first. For example, you get all the appdomain events before you get any assembly events. Originally, the design goal was for partial-process debugging (which the CLR doesn't support) to allow a debugger to only subscribe to debugging subsets of the debuggee.
  2. The attach is not considered complete until all the fake events are drained.  Specifically, the debugger does not have the full state of the debuggee until after attach is finished. Other operations may not work well before attach is complete.
  3. Unfortunately, there's no managed "Attach Complete" event to tell you when that point is. This makes it very difficult to attach, take a snapshot, and then detach, because you don't know when the attach is complete. MDbg does has some built-in heuristics to infer attach complete and generates a fake attach complete event. (As an aside: this is one of the things I like about MDbg. It lets us smooth over some warts in ICorDebug.) This is kind of what the DrainAttach() function does in this callstack snapshot tool. Visual Studio does not stop the IDE after attach, but it prevents you from doing an aysnc-break before the attach is complete.
  4. Native debugging is a little better because that has the "loader breakpoint", which is that first breakpoint (int 3 on x86) that occurs on startup. However, since int3s can occur before the loader breakpoint, there's no simple way to be 100% correct about that either.
  5. There is no good way to distinguish between a fake attach event and a real attach event. For example, if you attach to a pure native app and then it loads the runtime, you'll get real CreateXYZ events. If you attach to an already managed app, you'll get the faked up CreateXYZ events above. You may get the same events in both cases, but in one case the events are real and in the other they are faked. The nice part about this is that the same debug engine can then handle both scenarios. The bad part about this is that it is lying.