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.
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 DWORD
s everywhere, the CLR uses ULONG32
s. 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.
Comments