How the .NET Debugger Works

Process Information

We've got as far as debugging a simple application, but in order to get that far I had to skip the details of what was going on. These first sections are aimed at getting a working debugger up and running so that we can deal with the important features such as breakpoints, code stepping and watch windows. This time I'll get into a bit more detail about what we're being told about the process that we're debugging and add another little bit of information that VS.NET shows to add to the debug output that we displayed last time. First I'll explain more about how we start the application we are going to debug.

The following piece of code is taken from the startup code from last time.

    STARTUPINFOW startupInfo;
    PROCESS_INFORMATION processInfo;
    ZeroMemory(&startupInfo, sizeof(startupInfo));
    ZeroMemory(&processInfo, sizeof(processInfo));
    startupInfo.cb = sizeof(STARTUPINFOW);
    DWORD debugFlag = CREATE_NEW_CONSOLE;
    if(debugWin32)
    {
        debugFlag |= DEBUG_PROCESS;
    }
    hr = debug->CreateProcess(exeName, commandLine, NULL, NULL, FALSE, debugFlag,
                                NULL, startingPath, &startupInfo, &processInfo,
                                DEBUG_NO_SPECIAL_OPTIONS, &process);
    if(FAILED(hr))
    {
        return hr;
    }

This code is the method that starts our process. Anybody who is familiar with the Win32 CreateProcess call will find this very similar, with the parameter list being the same except for a new out parameter at the end.

The parameters that we pass are very much standard for a call like this, although you should note that the debuggging API takes UNICODE strings and so you must pass a pointer to the unicode version of the STARTUPINFO structure, and all the string parameters must be UNICODE too.

Parameter Description
const WCHAR* applicationName The executable location
WCHAR* commandLine The command line for the application
SECURITY_ATTRIBUTES* processAttributes Process security attributes
SECURITY_ATTRIBUTES* threadAttributes Thread security attributes
BOOL inheritHandles Should the new process inherit handles?
void* environment Pointer to an environment block for the application
DWORD creationFlags Pointer to an environment block for the application
const WCHAR* currentDirectory The working directory for the application
STARTUPINFOW* startupInfo Pointer to startup parameters
PROCESS_INFORMATION* processInformation Out parameter that contains information about the process
CorDebugCreateProcessFlags debuggingFlags CLR debugger configuration
CorDebugProcess** process Output parameter to a ICorDebugProcess object.

The parameters here are all pretty obvious, or can be safely ignored for the moment. For most of the parameters we are just passing NULL, or passing empty structures. The applicationName and commandLine parameters are self explanatory and the currentDirectory parameter sets the working directory for the debuggee.

The creationFlags parameter is used for two reasons. The first is to tell the debugging API that we should create a new console window for the debuggee, which we do in order to stop our debugger messages from being mixed up with the debuggee messages. There is an interesting feature here where if we pass DEBUG_PROCESS to this parameter we enable native Win32 debugging on this process. This enables the use of the ICorDebugUnmanagedCallback interface, but that will have to wait for another time.

The PROCESS_INFO parameter is used slightly differently than under Win32 debugging because we don't actually need to use in to tell when our debuggee has finished running. We will be receiving an ExitProcess notification when it finishes instead and so for now we don't have any need for the information in this structure.

The final two parameters are new parameters added for the CLR. The debuggingFlags provide us with some control over the features that the debugger will use. At the moment nothing is documented to work here, although DEBUG_ENABLE_EDIT_AND_CONTINUE is mentioned. Under 1.0 and 1.1 of .NET this will of course do nothing since edit and continue isn't implemented in these version, so we can expect this feature to make an appearance in version 2.0, although the mechanism might be a little different.

The final parameter is the ICorDebugProcess object that we get returned. This is an important object for us and so we store this for later.

The values that I pass here are enough for most uses of the API, which is why I've not gone into any detail about the other parameters here. Since they're the same as the Win32 call the documentation in MSDN covers them in more detail if you want to know more.

Getting more information

At the end of the last part we had a debugger that only reports back debug messages and the existence of events. This time we're going to take a look at what we are told during each of these events.

CreateProcess

The first notification that we received was CreateProcess. This supplies us with a parameter to an ICorDebugProcess object, which in this case points to the same object that is returned from the CreateProcess call earlier. This object allows us to retrieve information about the process such as the Win32 ID and the objects in the process. It also allows us to read memory from inside the process, and also to write to it. ICorDebugProcess derives from ICorDebugController, which provides us with the all important Continue call that we must make in most of the notification handlers in order to tell the CLR that we with the debuggee to continue processing.

For now the CreateProcess call isn't that interesting for us, but the process object will get used quite a lot as we get further into debuggers. At the moment we just call GetID() to retrieve a DWORD that contains the Win32 process ID.

CreateAppDomain

HRESULT TestDebugger::CreateAppDomain(
        /* [in] */ ICorDebugProcess *pProcess,
        /* [in] */ ICorDebugAppDomain *pAppDomain)
{
    std::cout << __FUNCTION__ << std::endl;
    pAppDomain->Attach();
   
    ULONG32 len = 0;
    WCHAR name[256];
    pAppDomain->GetName(256, &len, name);
    std::wcout << name << std::endl;
   
    GetProcess()->Continue(FALSE);
    return S_OK;
}

We're already handling this event because we needed to attach to AppDomains in order to receive any debug notifications about them. This notification receives another pointer to the process and a pointer to an ICorDebugAppDomain object. As you might guess this object allows us to view and control the application domain. AppDomains contain collections of assemblies, breakpoints and steppers as well as ways to get information about the AppDomain such as the name and id. At this point in time the AppDomain contains no assemblies, breakpoints or steppers. Again I'm going to duck out and say that I'll explain breakpoints and steppers in another article. AppDomains have names, and so we'll expand our CreateAppDomain handler to display the name to the console.

One thing to note is that where as Windows uses DWORDs everywhere, the CLR uses ULONG32s. This is obviously for reasons of clarity and portability, but since they are actually unsigned 32bit integers as far as we're concerned there's no real difference. The GetName function itself is simple and just takes the length of the string you pass, the UNICODE string to fill and we are returned the length of that string including the NULL. Once we retrieve this we just output it to the console. The single AppDomain that our test application creates is called "DefaultDomain".

LoadAssembly

Now that we have an AppDomain we start to get notifications that assemblies are being loaded. In our simple test application four assemblies are loaded, and we can retrieve their names in the same way as we did for the AppDomains to see what they actually are.

The code is almost identical to retrieving the name on the AppDomain, and if we run with it we see that the first assembly to load is mscorlib.dll, followed by TestApp1.exe and System.dll loads. Anybody who is familiar with the CLR would probably have been able to guess what the first three were going to be since mscorlib is needed in order to load our application, and System.dll is likely to be a good candidate for the next assembly that we need. The final assembly is System.Xml.dll and is a little less obvious. The answer is in our little debuggee test application. In order to make the code in the last article do anything interesting the line System.Diagnostics.Debug.WriteLine("Hello"); was added, and if we comment this out then both System.dll and System.Xml.dll no longer get loaded. From this we can assume that Debug.WriteLine either lives in System.dll, or uses code from both System.dll and System.Xml.dll. One of the nice things about .NET is that we can take a look to see if that's true by using a tool such as Reflector. Looking at the entry for mscorlib.dll shows us that Console.WriteLine exists there, as does System.Diagnostics.Debugger.IsAttached. System.Diagnostics.Debug exists in System.dll, which is interesting as you might expect all of the System namespace to exist in System.dll. What Microsoft has done is placed the commonly used code in mscorlib.dll as an optimisation and so these namespaces have ended being split up over several assemblies. Dissasembling Debug.WriteLine shows us that it just calls TraceInternal.WriteLine, which if we follow far enough down we see that it calls code in System.Xml.

LoadModule

Assemblies consist of one or more modules, and so if we adapt our existing GetName code yet again to call onto the ICorDebugModule object that is passed to the LoadModule notification we can see that each assembly that we load contains a single module that has a name that is the same as the assembly name. We will care more about this notification later.

CreateThread

This notification is pretty straight forwards, and all we do is retrieve the thread ID through the GetID() function. Because this in a Win32 thread ID it's a DWORD.

NameChange

The final type of notification we receive that isn't an Unload or Exit is NameChange. This is called when an AppDomain or Threads name changes. Since both of these objects are passed to the function you just check to see which is not NULL and then you can retrieve the new information about that object. In our case we are notified that the AppDomain “DefaultDomain” has been renamed to the more useful “TestApp1.exe” after the AppDomain has been initialised.

UnloadModule, UnloadAssembly, ExitThread and ExitAppDomain

These notifications are all pretty straight forward and aren't that interesting to us at the moment. The code with this article just displays the same information as their Create or Load versions so that we can see in what order everything is taken down.

ExitProcess

The ExitProcess notification is a very useful one for us since it tells us that we have stopped debugging and so in most cases it's the best place to reset the state of the debugger back to not debugging.

So far...

We now have a debugger to displays both debug messages and shows which assemblies are being loaded. That's two pieces of VS.NET functionality in as many articles. Check out "Part 2" in the download for the full source code so far. Admittedly they're both very cheap pieces of information to retrieve and so next time I'll start to get into something more interesting with breaking into the debuggee. Along the way we'll be loading and displaying code, stepping through code and creating breakpoints. I'll also clean up the display slightly because all these notifications to a console window don't rate very highly for usability.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.” - Donald Knuth