Enumerating all Modules in a Managed Process

I recently helped a developer that needed to enumerate all of the modules in an arbitrary process. They are given a Process ID (PID) and need to enumerate all of the modules, both native and managed that have been loaded by the process.

They started by creating a System.Diagnostics.Process object for the given PID with Process.GetProcessById. With the managed process object they were then able to get a collection of loaded modules with the Modules property. All well and good but they discovered that not all managed assemblies that are loaded in the process are included in the collection returned by the Modules property.

This is actually expected behavior for framework versions 4 and above. The .NET Framework 4 Migration Issues article in the section 'Assembly loading' describes this. It was an optimization created such that LoadLibrary is not used for managed assembly loading -- The MapViewOfFile Win32 api is used instead. An effect of this is that the windows loader does not see these module loads and Process.Modules, or native api that enumerate loaded modules via the Process Environment Block (PEB) are not aware of the assemblies.

We had looked at various resolutions to this problem -- attaching a managed debugger was considered. This would allow the enumeration of AppDomains and their assemblies which would give a list of all managed assemblies. This was rejected primarily because it would prevent debugging of the application after the managed debugger was attached.

The developer had also started investigating some undocumented ways of finding all file handles in the process and matching those with file names. SysInternals seems to use this method for some of their tools if you are interested in searching for that resolution. CSS cannot recommend or investigate solutions that use undocumented api -- these are subject to change at any time so it's a dangerous path.

A managed profiler would also be a way to capture managed module loads -- but that would require the profiler to be loaded in every managed process which was not a viable solution for this developer either.

Lee Culver suggested his excellent Microsoft Diagnostics Runtime (ClrMD) package as a way to accomplish this goal.

ClrMD is an open source project on GitHub:

https://github.com/Microsoft/clrmd

It's also available as a NuGet package:

<www.nuget.org/packages/Microsoft.Diagnostics.Runtime/0.8.31-beta>

This is still not officially released so when you search for it in the Visual Studio package manager, make sure you select the 'Include prerelease' checkbox so it can be found.

Here is a simple console app which uses ClrMD to enumerate the assemblies in an instance of Visual Studio. Add this code to a Wizard generated C# console app that has the ClrMD NuGet package added to the solution (Project menu, Manage NuGet Packages...).

 using System;
using System.Linq;
using System.Diagnostics;
using Microsoft.Diagnostics.Runtime;

/*
 This requires the Microsoft.Diagnostics.Runtime NuGet package (which 
 is marked pre-release, so it won't show up unless you check that box
 in NuGet).
*/

class Program
{
    static void Main(string[] args)
    {
        string processName = "devenv";
        int pid = Process.GetProcessesByName(processName).First().Id;

        /*
         The key here is to use the Passive attach flag.This will "attach" to
         the process by using OpenProcess / ReadProcessMemory but it does not 
         attach a debugger to it or pause the process in any way.
        */
        DataTarget dt;
        using (dt = DataTarget.AttachToProcess(pid, 5000, AttachFlag.Passive))
        {
            /*
             To see native module list, use dt.EnumerateModules.However, this
             is NOT what the customer was looking for, since it has the same 
             problem they report. Instead we will request the managed module 
             list from the runtime object.
            */

            // First, loop through each Clr in the process (there may be 
            // multiple in the side-by-side scenario).
            foreach (ClrInfo clrVersion in dt.ClrVersions)
            {
                ClrRuntime runtime = clrVersion.CreateRuntime();
                foreach (ClrModule module in runtime.Modules)
                {
                    if (module.IsFile)
                        Console.WriteLine(module.FileName);
                }

                /*
                 Note that ClrMD builds state in caches with every API call.
                 Since the process is live and constantly changing this means 
                 subsequent calls to ClrRuntime.Modules will get the same  
                 data back over and over. Instead you need to call 
                 ‘ClrRuntime.Flush’ if your reuse the runtime object to check 
                 the module list repeatedly:
                */

                /*
                 Uncomment this if you ever loop through ClrRuntime.Modules
                 again, expecting updated results.
                */
                // runtime.Flush();
            }
        }
    }
}