.NET Internals

Examine Running Processes Using Both Managed and Unmanaged Code

Christophe Nasarre

This article discusses:

  • Using the classes in the System.Diagnostics namespace
  • Using P/Invoke when necessary
  • Limitations of the ICorPublishxxx interfaces
  • Efficiently utilizing the debugging ICorDebugxxx interfaces
This article uses the following technologies:
C++, C#, COM, .NET

Code download available at:NETProcessBrowser.exe(651 KB)

Contents

Enumerating Processes
Process and Other Related Classes
ProcessModule, FileVersionInfo, and ProcessThread Classes
But I Need to Call Unmanaged Functions!
ICorPublish Interfaces Family and Profiling API
Building the AssemblyBrowser Tool
Enter the .NET Debugging API
AppDomain, Assembly, and Module
Deciphering Classes
Conclusion

Interested in debugging techniques? You've come to the right place. In this article, you'll discover how to write a utility called AssemblyBrowser, which is similar to the Unix ps tool. AssemblyBrowser allows you to browse managed processes, AppDomains, assemblies, modules, and even the classes loaded by the runtime. You'll see how to get a process' ID, name, priority, number of threads, kernel handle, and memory consumption, as well as its user-mode, kernel-mode, and total elapsed running time. AssemblyBrowser is based on the common language runtime (CLR) debugging services, which supplies a number of services that form the foundation of the tool.

In addition to the specifics of this tool's implementation, I'll also explain the notion of a "responding" process, and show how to extract Win32® Portable Executable (PE) file version information. For processes running on the same machine, you'll see how the Graphical Design Interface (GDI) and USER handle count, and the command line, can be obtained with P/Invoke function calls.

Back in the June 2002 issue of MSDN®Magazine I presented some ways to enumerate processes running on your system along with their loaded DLLs (in "Escape from DLL Hell with Custom Debugging and Instrumentation Tools and Utilities"). If you're developing with managed code, you don't need to use P/Invoke to access functions from psapi.dll or toolhelp32.dll in order to get a list of the running Win32 processes. For pure managed applications and for unmanaged applications hosting the CLR, the .NET Framework offers other ways to fetch information. As you'll discover, it's also easy to get a list of the DLLs loaded at specific addresses within a process, as well as a list of that process' executing threads with their ID, priority, execution state, and user/kernel time.

Enumerating Processes

The System.Diagnostics.Process class allows you to get the list of currently running Win32 processes, regardless of whether they're managed or unmanaged. By calling the static method Process.GetProcesses, you can retrieve an array of Process objects, each of which contains information about a running Win32 process. As an optional parameter to this method, you can specify the name of the machine in whose processes you are interested, and as a result there are three ways to get the list of processes running on the local machine: you can call the parameterless overload of the method, you can specify "." as the machine name, or you can explicitly provide the machine name as the parameter.

To learn the name of your machine, you simply can use the MachineName static property of the System.Environment class. Alternatively, you might be tempted to use the MachineName property of a Process object. Using the Process.GetCurrentProcess static method enables you to easily get access to a Process object wrapping the current application; however, its MachineName property returns ".". This is because each process retrieved via the GetProcessXxx methods has a MachineName property that is either the filter you provided or "." for the parameterless override.

It is interesting to note that the current implementation of the .NET Framework (version 1.1) relies on TOOLHELP32 for Windows 9x systems. Owing to the limitations of this API, it is not possible to access remote machines on these platforms. For Windows NT®, Windows® 2000, and Windows XP, the processes and threads information is fetched using performance counters, even for the local machine. While a slower solution, it allows you to reach remote computers. It is even possible to give the IP address of the remote machine instead of its name. For enumerating modules and DLLs, there is no access provided to those on a remote machine because the Microsoft® .NET Framework relies on some process status API (PSAPI) functions such as EnumProcessModules, GetModuleInformation, GetModuleBaseName, and GetModuleFileNameEx, which were described in detail by Matt Pietrek in his August 1996 Under the Hood column in Microsoft Systems Journal.

If you want to filter the processes by name, you can use another static method provided by the Process class, GetProcessesByName, passing the file name of the application, but without the .exe extension. This is not a regular expression filter, rather a "raw," case-insensitive comparison. So, for example, if you want to retrieve a Process instance for every Windows and Microsoft Internet Explorer process currently running, you'll have to make two calls to GetProcessByName: one specifying "explorer" for the processName parameter and another with "iexplore," instead of using a regular expression which would allow for only one call using the simpler parameter "explore". It isn't too complicated, however, to build your own extended version of GetProcessesByName that allows for filtering based on regular expressions, as shown in Figure 1. Invalid regular expressions, invalid remote machine names, and unsupported remote accesses will all cause exceptions to be thrown.

Figure 1 GetProcessesByName with a Regex

Process [] GetProcessesByName( string machine, string filter, RegexOptions options) { ArrayList processList = new ArrayList(); // Get the current processes Process [] runningProcesses = Process.GetProcesses(machine); // Find those that match the specified regular expression Regex processFilter = new Regex(filter, options); foreach(Process current in runningProcesses) { // Check for a match. if (processFilter.IsMatch(current.ProcessName)) { processList.Add(current); } // Dispose of any we're not keeping else current.Dispose(); } // Return the filtered list as an array return (Process[])processList.ToArray(typeof(Process)); }

Last, but not least, it is often handy to have a list of processes sorted by name or by ID. The lists you get from static Process methods, however, are not sorted in any particular order. Since these methods return an array, getting the expected sorted process lists should can be achieved by using the Array.Sort static method. For this to work, however, the objects stored in the array must either implement IComparable or you must provide an instance of a class that implements the IComparer interface, which is used to compare two objects. The former condition is not fulfilled because the Process class does not implement IComparable, nor does the .NET Framework offer a class that implements the latter condition. That said, it is easy enough to do it yourself. The following class can be used to sort an array of Process objects by name in alphabetical order:

public class ProcessComparer : IComparer { int IComparer.Compare(object x, object y) { return ((Process)x).ProcessName.CompareTo( ((Process)y).ProcessName); } }

With this class, it is now trivial to retrieve an alphabetically sorted list of all processes whose name includes the string "explore", as shown here:

Process [] processes = GetProcessesByName( ".", "explore", RegexOptions.IgnoreCase); Array.Sort(runningProcesses, new ProcessComparer()); foreach(Process currentProcess in processes) { Console.WriteLine(currentProcess.ProcessName); }

Process and Other Related Classes

Once you have a reference to a Process object, refer to Figure 2 to see how Process is related to the classes exposed through its various properties. Process.Modules returns an instance of ProcessModuleCollection, a list of loaded modules. Each module is represented by an instance of the ProcessModule class, each associated with its own FileVersionInfo object. Process.Threads returns a ProcessThreadCollection, composed of instances of the ProcessThread class, each representing one thread in the running process.

Figure 2 Processes, Modules, and Threads

Figure 2** Processes, Modules, and Threads **

To sum up, Figure 3 presents what you need to do to get the list of processes, their loaded modules, and their constituent threads. In this implementation, the process with an ID of 0 is skipped because this is a "fake" process without any meaningful detail, corresponding to the Idle process in the Task Manager. Before digging into modules and threads, note the wealth of information exposed by the Process class, shown in Figure 4. For more information on the data provided by many of these properties, see Inside Microsoft Windows 2000, Third Edition (Microsoft Press®, 2000), by Mark Russinovich and David Solomon.

Figure 4 Information Exposed by the Process Class

Modules
ProcessModule MainModule Module object wrapping the executable file. You should take care that it returns null for the "System" process (its Id is 4).
ProcessModuleCollection Modules Array of loaded DLLs. You should take care that it is empty for the "System" process (its Id is 4).
Threads
Type Property Description
ProcessThreadCollection Threads Array of started threads.
Time
Type Property Description
DateTime StartTime Moment when the process has been started.
DateTime ExitTime Moment when the process died; throws a System.InvalidOperationException if the process is still running: "Process must exit before requested information can be determined."
TimeSpan PrivilegedProcessorTime CPU time spent in Kernel mode.
TimeSpan UserProcessorTime CPU time spent in User mode.
TimeSpan TotalProcessorTime Sum of the two previous properties.
Memory
Type Property Description
IntPtr MaxWorkingSet Maximum allowable working set size.
IntPtr MinWorkingSet Minimum allowable working set size.
int NonpagedSystemMemorySize Nonpaged system memory size allocated for the process.
int PagedMemorySize Paged memory size.
int PagedSystemMemorySize Paged system memory size for the process.
int PeakPagedMemorySize Peak paged memory size for the process.
int PeakVirtualMemorySize Peak virtual memory size for the process.
int PeakWorkingSet Peak working set size for the process.
int PrivateMemorySize Private memory size for the process.
int VirtualMemorySize Virtual memory size for the process.
int WorkingSet Working set size for the process.
Other
Type Property Description
int BasePriority Base scheduling priority.
int ExitCode Exit code; throws a System.InvalidOperationException if the process is still running: "Process must exit before requested information can be determined."
IntPtr Handle Handle of the process with PROCESS_ALL_ACCESS rights.
int HandleCount Number of allocated kernel objects.
int Id Unique ID.
string MachineName Matches the machine name used for the enumeration or "." for the parameterless method call.
IntPtr MainWindowHandle Handle of the main window.
string MainWindowTitle Title of the main window.
bool PriorityBoostEnabled Direct call to the Win32 GetProcessPriorityBoost and SetProcessPriorityBoost APIs.
ProcessPriorityClass PriorityClass Direct call to the Win32 GetPriorityClass and SetPriorityClass APIs.
string ProcessName Name of the executable file.
IntPtr ProcessorAffinity Direct call to the Win32 GetProcessAffinityMask and SetProcessAffinityMask APIs.
bool Responding Returns true if the main window of the application is responding to messages. This is the same detail the Status column shows in the Applications tab of the Windows Task Manager.

Figure 3 Simplified Source Code for ps.exe

Process[] runningProcesses = Process.GetProcesses("."); // sort the list if needed using a ProcessComparer object foreach(Process currentProcess in runningProcesses) { // skip Idle process if (CurrentProcess.Id == 0) continue; // dump details of the process // ... // list the loaded modules for this process ProcessModuleCollection loadedModules = CurrentProcess.Modules; foreach (ProcessModule module in loadedModules) { // dump module details DumpModule(Module); } // list the running threads ProcessThreadCollection RunningThreads = CurrentProcess.Threads; foreach (ProcessThread thread in runningThreads) { // dump Thread details DumpThread(thread); } }

None of the following members should be used when enumerating existing processes. Rather, they should be used when you create a process yourself:

ProcessStartInfo StartInfo StreamReader StandardError StreamWriter StandardInput StreamReader StandardOutput

ProcessModule, FileVersionInfo, and ProcessThread Classes

The Modules property of a Process object allows you to list the modules loaded by the application through a strongly typed ProcessModuleCollection. As with most collections, this class exposes typed accessors such as Contains, which in the case of ProcessModuleCollection takes a ProcessModule and returns true if this object is already in the list. Because of the reference semantics of the .NET Framework, if you get a Module from a first ProcessModuleCollection and you need to check whether it is loaded into another ProcessModuleCollection, Contains will return false. That's because this method does not compare the BaseAddress and the ModuleName for the wrapped process; rather, it compares the reference of each ProcessModule object itself. Each module is easily accessed through the bracket array syntax and is wrapped by a ProcessModule object with the properties listed in Figure 5.

Figure 5 ProcessModule Details

Type Property Description
IntPtr BaseAddress Address where the module is loaded into the process address space (not the one specified at link time).
IntPtr EntryPointAddress Address of the entry point (either DllMain or Main).
string FileName Full path name of the binary file with its extension.
FileVersionInfo FileVersionInfo Wrapper class for the Win32 version embedded into the binary file. Using it, you get access to the same information as displayed by the File Properties dialog box in Windows Explorer.
int ModuleMemorySize Space used in memory by the PE file.
string ModuleName Name of the binary file with its extension.

Some processes have unfamiliar, non-Win32 file names. For example, "\??\C:\WINDOWS\system32\csrss.exe" is returned for the Win32 subsystem process; "\SystemRoot\System32\smss.exe" is what you get for the Session Manager process. This is not a problem in itself except when you try to access the module's FileVersionInfo. In fact, this ProcessModule property is dynamically computed based on its File name. For the ill-formed cases, you'll end up having to deal with either a System.IO.FileNotFoundException or a System.ArgumentException specifying that there were "Illegal characters in path".

The Process class provides access to its threads through the Threads property—another strongly typed collection represented by ProcessThreadCollection. This class exposes more methods than ProcessModuleCollection, including such methods as Add, Insert, and Remove. I don't see any interest in having these members defined as public, since this threads list should be treated as a read-only snapshot of the process state. Instead, you can use the collection's CopyTo method to get a clean and independent copy of the list for your own purpose.

Now you can retrieve a ProcessThread instance for each thread running in the process. I have a comment to add here to the .NET Framework documentation. Not all of the properties will return a value initially computed when the ProcessThreadCollection is taken, as Figure 6 illustrates. So far, I have not been able to figure out why PriorityLevel gets a different treatment than ThreadState or TotalProcessorTime. This class also provides a ResetIdealProcessor method that permits you to indicate that there is no single ideal processor for this thread.

Figure 6 ProcessThread Details

Type Property Computation Time
int BasePriority Snapshot
int CurrentPriority Snapshot
int Id Snapshot
IntPtr StartAddress Snapshot
DateTime StartTime Snapshot
ThreadState ThreadState Snapshot
TimeSpan PrivilegedProcessorTime Snapshot
TimeSpan UserProcessorTime Snapshot
TimeSpan TotalProcessorTime Snapshot
ThreadWaitReason WaitReason Snapshot
int IdealProcessor Set on demand
bool PriorityBoostEnabled Get/set on demand
ThreadPriorityLevel PriorityLevel Get/set on demand

Last, but not least, you should take care to read the value of WaitReason if and only if ThreadState is set to the ThreadState.Wait value. Otherwise, be prepared to catch a System.InvalidOperationException exception.

But I Need to Call Unmanaged Functions!

Most of the details provided by the Windows Task Manager are available through the Process, ProcessModule, and ProcessThread classes. If you need a more esoteric piece of information, you can still rely on P/Invoke to call unmanaged Windows APIs directly. Following the example of the .NET Framework, I have built a class called Wrappers whose single purpose is to hide the implementation details needed to access some unmanaged functions.

To call an unmanaged function exported by a DLL, you can define a static extern method signature decorated with the DllImport attribute. This attribute takes the name of the DLL exporting the function as a mandatory parameter. This is a P/Invoke declaration for the CloseHandle function exported by kernel32.dll.

[DllImport("KERNEL32.DLL")] private static extern int CloseHandle(int hObject);

The major problem when using P/Invoke is not being able to find the name of the function you know so well in Win32 or the function's exporting DLL. Even if you type the wrong name, your code will throw a System.EntryPointNotFoundException with an explanation that reads something like, "Unable to find an entry point named [your function name] in DLL [name you gave to the DllImport file]."

You really begin to scratch your head when you need to "guess" how to define the parameters passed or returned by the function, and what managed types match the Win32 unmanaged types and structures. Adam Nathan has created the site at https://www.pinvoke.net to help developers with this very problem. In addition, .NET Framework assemblies such as system.dll or mscorlib.dll contain some classes whose sole purpose is to provide a polished managed view of P/Invoke calls to well-known unmanaged APIs, as Figure 7 shows. If you double-click on one of these methods, you'll get a managed signature needed for your DllImport-tagged method. (Lutz Roeder's .NET Reflector tool provides the same kind of details in C#, Visual Basic® .NET, or Delphi syntax.) The wrappers.cs file in the code download for this article contains the managed methods that correspond to the APIs explored in my aforementioned June 2002 article, such as GetProcessMemoryInfo, NtQueryInformationProcess, and even ReadProcessMemory which can be used to get the command line that launched a process.

Figure 7 ILDASM and P/Invoke Wrapper Classes System.dll

In addition to the managed classes explored thus far, the .NET Framework offers unmanaged ways to fetch information about running managed applications or unmanaged applications hosting the CLR. I'll now examine a tool based on the CLR debugging services that allows you to browse managed processes, AppDomains, assemblies, modules and even the classes loaded by the runtime.

ICorPublish Interfaces Family and Profiling API

If you are only interested in managed processes or processes hosting the CLR, the classes introduced so far don't give you a way to distinguish between plain unmanaged Win32 applications and the ones you are interested in. Of course, you could cheat and assume that the processes with mscoree.dll or mscorlib.dll loaded in their ProcessModuleCollection are .NET processes, but that won't work correctly, since an application can dynamically load these DLLs to directly call an undocumented function under the control of the CLR.

One simple solution is documented in the debugging API reference guide DebugRef.doc, which is stored in the Tool Developers Guide\docs Framework SDK subfolder of your Visual Studio® .NET installation. A set of COM interfaces, found in the Process Publishing Services, can be used to list .NET processes and their application domains on the local machine. For sample code, my own reusable C++ implementation can be found in ManagedProcess.cpp of this article's code download (for UnmanagedProcesses). Also take a look at Chris Sells's Web site, https://www.sellsbrothers.com/tools, or see "CLR Debugging: Improve Your Understanding of .NET Internals by Building a Debugger for Managed Code," by Mike Pellegrino (MSDN Magazine, November 2002). Mike Woodring provides a really helpful managed wrapper example at ICorPublishProcess, ICorPublishAppDomain Interop Shims.

These interfaces are fine in themselves, but they only provide you access to the .NET processes and their AppDomains. There is no way to get access to their assemblies, modules, or loaded classes through these interfaces. If this is not enough for you, profiling might be the next step. As Anastasios Kasiolas explained in ".NET CLR Profiling Services: Track Your Managed Components to Boost Application Performance" (MSDN Magazine, November 2001), if you build a COM object that implements the ICorProfilerCallback interface, it will be called by the CLR when a variety of events occur in a profiled process. These events include application domain creation or shutdown, assembly loading or unloading, module loading or unloading, and class loading or unloading—exactly the type of information we need!

In fact, there are a few minor issues to overcome in order to use ICorProfilerCallback. First, only one profiler can be alive at a time. Second, your code is implemented as a COM server DLL, not as an executable. In addition to standard COM registration, you have to define two system-wide environment variables to let the .NET runtime know that your COM object is the one to be notified during the execution of all .NET processes. Another solution is to let your profiler run all the time and use a private communication channel to an application dedicated to presenting the profiled .NET processes in a nice user interface.

In addition to the complexity of the implementation and installation of such a solution, you may face performance problems because your profiler is called every time an event you are interested in occurs in each and every process. This final issue is especially relevant for the events related to the loading of classes, an event which can happen frequently.

Building the AssemblyBrowser Tool

In the August 2002 issue, I explained how to use the Win32 debugging API to build a tool that accurately lists the DLLs that are statically or dynamically loaded and unloaded for a debugged process. The major constraint of this tool is that you need to start the target application under a debugger and control its lifetime from birth to death. I want to take a snapshot of already-running processes. Fortunately, the .NET debugging API allows a piece of code to attach to any .NET process, receive the notifications needed about already loaded AppDomains, assemblies, modules, and classes. And to top it off, you can detach from the debuggee without killing it, a capability the Win32 API did not have until Windows XP. I'll explain now how to use the .NET debugging API to build AssemblyBrowser. This tool lists .NET processes, their AppDomains, assemblies, modules, and loaded classes.

AssemblyBrowser is merely a user interface sitting on top of several reusable classes. It's built using a simple object model, as you can see in Figure 8. Each wrapper class defined in ManagedProcesses.h is built following the same model: a set of read-only property-like methods describing the underlying object (Process, AppDomain, Assembly, Module, Classes) and the GetFirst/GetNext enumerator helpers to go deeper into the hierarchy.

Figure 8 AssemblyBrowser Object Model

Figure 8** AssemblyBrowser Object Model **

The class root, CManagedProcessList, is responsible for building a snapshot of the running .NET processes, based on the Publishing API already described. The class CManagedProcess is constructed via an ICorPublishProcess interface pointer, from which its PID and full path name are retrieved. Since the IsManaged method always returns true, there is no way through ICorPublishProcess to determine if a .NET process is managed by the CLR or is unmanaged but hosting the CLR. A good rule of thumb would be to take a look at its PE format and try to find out if it references kernel32.dll. If this is the case, chances are that the process is hosting the CLR.

The next task is to determine how you can enumerate the AppDomains created by the process. You could call the EnumAppDomains method from ICorPublishProcess to get a pointer to an ICorPublishAppDomainEnum enumeration interface. With it, you simply call Next until you get the last ICorPublishAppDomain interface pointer. Calling its GetID and GetName methods allow you to take a picture of each AppDomain. For AssemblyBrowser, another solution is used, which I'll cover now.

Enter the .NET Debugging API

First, you need to obtain an ICorDebug interface pointer through CoCreateInstance and call its Initialize method. Before leaving the application, and before releasing this interface, don't forget to call its Terminate method. I wrapped this central interface inside the CManagedDebugger class. Only one instance is available, following a singleton pattern. You simply have to initialize the COM runtime before calling the global function InitEngine, and don't forget to call ShutdownEngine before unloading the COM runtime with CoUninitialize.

Once you have a pointer to an ICorDebug interface, you may be tempted to call its GetProcess member with the process ID as a parameter to retrieve a pointer to the ICorDebugProcess wrapping your process. Unfortunately, this method returns E_INVALIDARG. This is because the debugger is not attached yet, so the application has not been debugged. You face the same problem if you try to enumerate processes with EnumerateProcesses, since it returns S_OK, but the ICorDebugProcessEnum interface pointer you get is useless. Its GetCount method indicates 0 and GetNext returns E_INVALIDARG.

But this is not how ICorDebug works. This interface allows you to either start a new process with CreateProcess or to attach to a running application with DebugActiveProcess. In the November 2002 article I mentioned earlier, Mike Pellegrino explains how to use the former and how to build a script debugger based on ICorDebug. The latter is used in a different way, as I'll demonstrate.

When I started reading the documentation and working with the IDL for ICorDebug, I followed the dead-end path shown in Figure 9. When I called DebugActiveProcess with the ID of the .NET process in which I was interested, I had no problem getting it to return a pointer to an ICorDebugProcess interface. Unfortunately, when I tried to call its EnumerateAppDomains method, its count was 0. This was only the beginning of my problems.

Figure 9 The Wrong Way to Use DebugActiveProcess

ICorDebugProcess* pDebuggee = NULL; HRESULT hr = pICorDebug->DebugActiveProcess(PID, L"We don't debug at the Win32 level" == NULL, &pDebuggee); if (FAILED(hr) || (pDebuggee == NULL)) { TRACE("AttachDetachDebuggee() - " "impossible to get process %u...\n", PID); return(hr); } ICorDebugAppDomainEnum* pAppDomainEnum = NULL; hr = pDebuggee->EnumerateAppDomains(&pAppDomainEnum); if (FAILED(hr)) { TRACE("AttachDetachDebuggee() - " "impossible to enumerate AppDomains %u...\n", PID); return(hr); } // try to list AppDomains ULONG AppDomainCount = 0; hr = pAppDomainEnum->GetCount(&AppDomainCount); if (FAILED(hr)) { TRACE("AttachDetachDebuggee() - impossible to get " "the AppDomains count for process %u...\n", PID); } //... list AppDomains hr = pDebuggee->Detach(); if (FAILED(hr)) { TRACE("AttachDetachDebuggee() - " "impossible to detach from process %u...\n", PID); } pDebuggee->Release();

The call to Detach returned an error and the debuggee was killed because I had neglected a requirement: it is mandatory to first register a callback through ICorDebug::SetManagedHandler. Before you call DebugActiveProcess, you have to register your class which implements the ICorDebugManagedCallback interface. Each of its methods is called by the CLR debugging services when an event occurs within the debuggee. Figure 10 lists the callbacks related to AssemblyBrowser's goals. In DebuggerCallback.cpp, you can uncomment several _TRACE_XXX flags (where XXX is PROCESS, APPDOMAIN, ASSEMBLY, MODULE, CLASS, and THREAD) to allow fine-grained tracing in each handler. This is a good way for a developer to understand when each is called and what arguments it is called with.

Figure 10 Managed Debugging API Workflow

Method Parameters Note
CreateProcess ICorDebugProcess* Called once just after DebugActiveProcess
CreateAppDomain ICorDebugProcess*
ICorDebugAppDomain*
Called once per AppDomain in the process
LoadAssembly ICorDebugAppDomain*
ICorDebugAssembly*
Called once per Assembly in each AppDomain
LoadModule ICorDebugAppDomain*
ICorDebugModule*
Called once per Module for each Assembly
LoadClass ICorDebugAppDomain*
ICorDebugClass*
Called once per class
CreateThread ICorDebugAppDomain*
ICorDebugThread*
Called once per thread in the process

It is also possible to act as a Win32 debugger and thus to be notified of the corresponding unmanaged debugging events. You would register an implementation of the ICorDebugUnmanagedCallback interface through ICorDebug::SetUnManagedHandler, but we don't need to do that for AssemblyBrowser.

In the Win32 debugging world, once you have started to debug an application, either by launching it or by attaching to a running process, you need to call WaitForDebugEvent to wait for the next event to happen in the debugged app, and ContinueDebugEvent when you have finished handling this particular event. This continues until the debuggee ends or until you decide to leave the game. Your debugging logic stays in the thread waiting for an event to occur in the debuggee.

This process is totally different in the managed world, as Figure 11 illustrates. After you attach to a debuggee, there is no need to call a particular ICorDebug or ICorDebugProcess method in order to receive the events. The ICorDebugManagedCallback methods you have implemented are called from another thread each time an event occurs in the debuggee. Each method receives an ICorDebugController inherited interface pointer as a parameter, such as ICorDebugProcess or ICorDebugAppDomain as context. Once you have finished working on an event, it is your responsibility to call the Continue method of this parameter so the debuggee can keep on running.

Figure 11 Debugger/Debugee Interactions

Figure 11** Debugger/Debugee Interactions **

These techniques have one limitation: the CLR does not allow you to attach to a process that's already being debugged. The direct consequence of this limitation is that you can't get information related to one of these processes. But there is another, less obvious consequence. Since Visual Studio .NET is hosting the CLR, it becomes impossible to debug your own code. When a breakpoint that you have added into the code (somewhere before detaching) is triggered, both applications are deadlocked. Visual Studio .NET will never be able to call ContinueDebugEvent to let AssemblyBrowser call Continue.

This is also the reason why you can find the following code in CManagedProcess::AttachDetachDebuggee:

if (::IsDebuggerPresent()) { if (wcsstr(m_szPathname, L"devenv.exe") != NULL) return(S_OK); }

Replace "devenv.exe" with the file name of your own debugger if you are not using Visual Studio.

The last architectural issue you need to tackle is how you can synchronize the main thread with your callback so that you can detach from the debuggee when the last interesting event is handled. When you are debugging by attachment, the last event is in fact received by your CreateThread handler. Unfortunately, since more than one thread is always running in any managed process, you can't tell how many times CreateThread will be called. This means you can't detach immediately after the first thread notification. To solve this problem, I created a Win32 Event object named m_hEventNext, which is created before attaching to the debuggee and is signaled in the CreateThread handler. The trick is to call Continue in the handlers of the callback until the first thread is detected, then call Continue in the main thread until ICorDebugProcess::HasQueuedCallbacks returns false, meaning no more events are available in the debuggee. At that point, the main thread can safely detach from the debuggee using the Detach method.

AppDomain, Assembly, and Module

With this architecture in place, the details we are interested in are gathered through the handler methods that were listed in Figure 2 (in the same order). A single CreateProcess is called, and then one CreateAppDomain call is triggered for each AppDomain already created in the process. Each loaded assembly is then signaled by a LoadAssembly call before one LoadModule call per module is issued. To keep track of this stack of calls in the callback class, I store two objects: the CAppDomain object that wraps the current AppDomain in its m_pCurrentAppDomain member, and the CAssembly object that wraps the current assembly in its m_pCurrentAssembly member.

After this set of notifications, your callback LoadClass method is called for each class loaded by the CLR in the process. Finally, your CreateThread method is triggered for each thread detected.

So as not to walk through the source code line by line, let's look at the major implementation details related to each particular handler. In CreateProcess, the m_bHasSeenFirstThread Boolean member is set to false, waiting for CreateThread to reset its value to true. In CreateAppDomain, Attach needs to be called for the given AppDomain, otherwise you will not be notified for its assemblies and modules. Since we are dealing with COM, we need to AddRef this interface, meaning it's necessary to store the corresponding interface pointer in order to call Detach and Release on it once a new AppDomain is detected, or before detaching from the debuggee, as in the case of the last AppDomain.

Sometimes, the ICorDebugAssembly interface pointer given as the parameter to LoadAssembly has the bad habit of returning an empty name through its GetName method. In that case, "<Unknown or dynamic assembly>" is returned as the name of the wrapping CAssembly object. This situation often arises for assemblies dynamically created by Reflection.Emit. According to DebugRef.doc, the GetCodeBase method is not supposed to be implemented. If you try to call it, it returns E_NOTIMPL. This is not important; if you need to know the path corresponding to the assembly, it is returned by GetName.

In the case of LoadModule, you get an ICorDebugModule interface pointer describing each module loaded in the current assembly. Using the GetName method, you can retrieve its full path name. GetBaseAddress returns the address where it has been loaded in the process address space. In the case of a dynamically created AppDomain, for the corresponding modules, IsDynamic returns true and IsInMemory distinguishes between in-memory and on-disk dynamic modules.

Deciphering Classes

If you want to be notified for the loaded classes, you have to explicitly call EnableClassLoadCallbacks on the ICorDebugModule received as a parameter of LoadModule. Otherwise, by default, your LoadClass handler is never called. The CLR debugging services were designed that way for better performance. Since plenty of classes are loaded, your handler will be called hundreds of times, and if you don't need this information, you don't want to pay the performance penalty associated with obtaining it. You should also note that when you are using a particular type, a bunch of other types are implicitly loaded, such as all the types in its inheritance hierarchy as well as the types for any implemented interfaces.

In addition to an ICorDebugAppDomain interface pointer, the ClassLoad method receives an ICorDebugClass interface pointer as a parameter. When the time comes to identify the corresponding type, you can rely on its GetToken method, which returns an mdTypeDef object. For AssemblyBrowser, given a type definition token we need to get its namespace, its outer classes if it's nested, and the type itself. To translate what is returned by GetToken, you need to switch to the unmanaged metadata API detailed in Metadata Unmanaged API.doc as well as by Matt Pietrek in "Avoiding DLL Hell: Introducing Application Metadata in the Microsoft .NET Framework," (MSDN Magazine, October 2000).

The IMetaDataImport interface is the cornerstone for discovering the metadata related to a type. It is usually obtained from the IMetaDataDispenser root interface given an assembly file name. When using the CLR debugging services, you can take a different approach. The GetModule method from ICorDebugClass lets you obtain a pointer to the ICorDebugModule interface wrapping the module defining the type. Call its GetMetaDataInterface method and request the corresponding IMetaDataImport interface. The full name of the type is discovered when you call its GetTypeDefProps method, passing the token representing the type. The token corresponding to its parent is also available through the last parameter of this method. In cases where a type is not defined in the same module, it may be quite a difficult undertaking to get information about it, since GetTypeProps is useless even if it returns S_FALSE as an expected failure.

If you are looking for additional information for the type, such as its namespace, parsing the full name is not enough since you won't be able to tell the difference between a namespace and a nested type. In other words, how do you decrypt foo.bar? Well, foo can be a type or a namespace. The IMetaDataImport interface does not seem to provide any way to get this kind of information directly. If you are feeling energetic, you should take some time to poke around inside the source code of ILDASM, installed with Rotor, a multiplatform shared-source CLI implementation of the CLR, available at Shared Source Common Language Infrastructure 1.0 Release. ILDASM relies on an undocumented IMDInternalImport interface that provides a GetNameOfTypeDef method. GetNameOfTypeDef returns both the namespace and the name of a class, given its token definition. Unfortunately, explaining how to use this interface in the context of the CLR debugging services is far beyond the scope of this article. You will, however, find some hints within the implementation of the CManagedType constructor in ManagedProcesses.cpp.

It is possible to build your own "smart parsing" based on a combination of common sense and another method from the IMetaDataImport interface. The source code listed in Figure 12 shows such a GetClassName method from ManagedProcesses.cpp.

Figure 12 Extracting Namespace and Nested Type from a Type Token

// Given a TypeDef token, return the corresponding type name as well as // its parent class and namespace. This method is intended as an // internal method only and assumes the supplied buffers are of the // correct size (_MAX_CLASS_NAME+1). HRESULT CManagedType::GetClassName( IMetaDataImport* pIMetaDataImport, mdTypeDef tkType, wchar_t* szClass, wchar_t* szParentClass, wchar_t* szNamespace ) { HRESULT hr = S_OK; wchar_t szFullClassName[_MAX_CLASS_NAME+1]; szFullClassName[_MAX_CLASS_NAME] = L'\0'; ULONG Length = 0; CorTypeAttr tdFlags = tdClass; // 0 value mdToken tkParent = 0; hr = pIMetaDataImport->GetTypeDefProps( tkType, szFullClassName, _MAX_CLASS_NAME, &Length, (DWORD*)&tdFlags, &tkParent ); // This could be S_FALSE in some cases. ASSERT(SUCCEEDED(hr)); if (FAILED(hr)) return(hr); // The full name is retrieved. If it's a nested type, szFullClassName // is the class name only, and I need to do the same for its parent. // Otherwise, it contains the namespace plus the class name at the end. if (!IsTdNested(tdFlags)) { // Not a nested type: // --> szFullClassName = namespace.namespace...namespace.ClassName wchar_t* pszNamespace = szFullClassName; wchar_t* pszDot = wcsrchr(szFullClassName, L'.'); if (pszDot == NULL) { // No namespace, just the class name szNamespace[0] = L'\0'; wcsncpy(szClass, szFullClassName, _MAX_CLASS_NAME); } else { // The '.' is just between the namespaces and the class name. // The namespace itself starts after the '.'. *pszDot = L'\0'; wchar_t* pszName = pszDot + 1; // Extract class and namespace wcsncpy(szClass, pszName, _MAX_CLASS_NAME); wcsncpy(szNamespace, pszNamespace, _MAX_CLASS_NAME); } // and no parent class szParentClass[0] = L'\0'; } else { // Get the token for its parent hr = pIMetaDataImport->GetNestedClassProps(tkType, &tkParent); ASSERT(SUCCEEDED(hr)); // szFullClassName is the class name and we have // to search recursively starting from its parent. wchar_t szTmpClass[_MAX_CLASS_NAME+1]; szTmpClass[_MAX_CLASS_NAME] = L'\0'; wchar_t szTmpParentClass[_MAX_CLASS_NAME+1]; szTmpParentClass[_MAX_CLASS_NAME] = L'\0'; hr = GetClassName(pIMetaDataImport, tkParent, szTmpClass, szTmpParentClass, szNamespace); ASSERT(SUCCEEDED(hr)); // build the class name from its parent name if (szTmpParentClass[0] != L'\0') { size_t length = 0; wcsncpy(szParentClass, szTmpParentClass, _MAX_CLASS_NAME); length = wcslen(szTmpParentClass); // count string just copied wcsncat(szParentClass, L".", _MAX_CLASS_NAME - length); length++; // count the '.' wcsncat(szParentClass, szTmpClass, _MAX_CLASS_NAME - length); } else wcsncpy(szParentClass, szTmpClass, _MAX_CLASS_NAME); // The single class name is returned by GetTypeDefProps() wcscpy(szClass, szFullClassName, _MAX_CLASS_NAME); // Nothing special to do for namespace as it was already // computed during the parent lookup. } return(hr); }

The basic idea is quite simple. If a type is not nested, all the words before the last dot are the namespace where it is defined. If it is nested, you need to iterate through the containing types until you reach the innermost type.

In the call to GetTypeDefProps with the type token, the fifth parameter is used to return a flag providing additional information, such as its visibility or whether it is an interface. Calling IsTdNested (defined in CorHdr.h) on this flag returns true if it is a nested type. If it returns false, you know that you only have to deal with namespaces separated by dots within the string returned by GetTypeDefProps. If this is a nested type, you simply have to apply the same idea on the containing type.

With a little help from the GetNestedClassProps method of IMetaDataImport, you can get the token corresponding to the type's enclosing type. You should notice that the name returned by the GetTypeDefProps method is actually the name of the type if the type is nested.

Conclusion

The classes in the System.Diagnostics namespace provide much of the same features as PSAPI and toolhelp. What's more, these classes are gathered into an object model that's not only much easier to understand but also easier to use. Of course, if your needs go beyond what the .NET Framework provides, you can back into the Win32 world using P/Invoke.

The CLR provides several COM interfaces to let you list .NET applications and their content. As you have seen, the ICorDebug set of interfaces opens up the debugging API, leaving you free to use what you need without having to write a real debugger.

Christophe Nasarreis a development manager for Business Objects in France. He has written several low-level tools for Windows since version 3.0 and reviewed many books on Win32, COM, MFC, and .NET. He can be reached at cnasarre@hotmail.com.