Introduction
This article describes a tool I wrote to show, pictorially and dynamically, the consumption of virtual memory by a Windows process.
Background
Recently, I needed to investigate the way in which a Windows process was consuming Virtual Memory (VM). I wanted to get a picture in my head of the available VM and how it was being allocated, freed and mapped by the process, and phenomena such as virtual memory fragmentation.
I came across this tool by Charles Bailey: http://hashpling.org/asm/. It works well and it helped, but I wanted more information about particular types of allocation (those corresponding to memory-mapped files and managed and unmanaged assemblies loaded by the process), and I wanted to be able to understand for myself what was going on 'under the hood'. So I wrote my own tool.
Using the Tool
Simply run the executable. It requires the .NET Framework 4 Client profile, but otherwise needs no installation.
Select a running process from the dropdown list. Alternatively, type the name of a process (e.g. "Excel"), and wait for it to start; Mnemonic will automatically scan the process for as long as it runs.
You'll see a screen such as this:
Note that the 'scale' at the right-hand side of the graph shows 3071 MB - approximately 3GB. This is because this screenshot was running on a 32-bit Windows machine with the /3GB switch, which extends the user-mode virtual address space to nearly 3GB. On a standard 32-bit Windows environment, this limit would be 2GB, whereas under 64-bit Windows it would be 4GB (because Chrome is a Win32 process).
Note the large (approximately 1GB) yellow region to the right, which is reserved but not committed. You'll see this if you run a Win32 process under 32-bit Windows with /3GB, or under 64-bit Windows, unless your process is marked as 'large address aware'. If your process is marked with LARGEADDRESSAWARE
, this region will be marked as Free (initially, at least); if not, Windows reserves it to prevent the process from accessing it.
Run your mouse over the graph to see details about the allocations, including address range and any module loaded (managed or unmanaged) at that range.
Control-click the graph to save it as a file. Specify the root name (and folder) for the files; a sequence number and the .png extension will be appended. Simply click the graph to save subsequent snapshots; the sequence number will increment.
Using the Code
The code consists of a reusable class for enumerating the contents of Virtual Memory: VirtualMemoryExplorer
, and a simple front-end interface that allows the selection of a process and the graphical presentation of the information.
Two 'enumeration' loops take place within VirtualMemoryExplorer.Scan
that build lists of VM regions that are of interest, and their characteristics.
The first looks at VM allocations:
UInt32 address = 0;
for (; ; address = (UInt32)m.BaseAddress + (UInt32)m.RegionSize)
{
if (0 == VirtualQueryEx(processHandle, (UIntPtr)address, out m,
(uint)Marshal.SizeOf(m)))
{
addressLimit = address;
break;
}
VMChunkInfo chunk = new VMChunkInfo();
chunk.regionAddress = (UInt32)m.BaseAddress;
chunk.regionSize = (UInt32)m.RegionSize;
chunk.type = (PageType)m.Type;
chunk.state = (PageState)m.State;
if ((chunk.type == PageType.Image) || (chunk.type == PageType.Mapped))
{
string fileName = GetMappedFileName(processHandle, chunk.regionAddress);
if (fileName.Length > 0)
{
fileName = Path.GetFileName(fileName);
chunk.regionName = fileName;
}
}
chunkInfos.Add(chunk);
};
The second looks at the modules (DLLs) loaded by the process. I used the Process.Modules
method. As per this page, from .NET 4, this list no longer includes managed assemblies - only unmanaged ones. (The only way to discover the managed assemblies is to use GetMappedFileName
in conjunction with VirtualQueryEx
, which is done in the snippet above).
mappingInfos = new List<VMRegionInfo>();
foreach (ProcessModule module in process.Modules)
{
VMRegionInfo mappingInfo = new VMRegionInfo();
mappingInfo.regionAddress = (UInt32)module.BaseAddress;
mappingInfo.regionSize = (UInt32)module.ModuleMemorySize;
mappingInfo.regionName = Path.GetFileName(module.FileName);
mappingInfos.Add(mappingInfo);
}
mappingInfos.Sort(delegate(VMRegionInfo map1, VMRegionInfo map2)
{
return Comparer<UInt32>.Default.Compare(map1.regionAddress, map2.regionAddress);
});
Points of Interest
I struggled with the process selection mechanism, which uses a DropDown
. If the dropdown button is pressed, I wanted to show the list of running processes. If a name is typed into the textbox
, I wanted Mnemonic to start scanning a process matching the typed name as soon as it starts. To achieve the behaviour I wanted, I eventually opted to scan for available processes in a background thread.
It was interesting to see that .NET applications are not, by default, marked as LARGEADDRESSAWARE
and that mainstream applications such as the Office 2007 suite are not so marked, either. I'd be interested to know why this is.
I'm curious about the possibility of using memory-mapped files in managed applications (there is new support for this technology in .NET 4), so I was interested to see that the CLR uses memory-mapped files to access assemblies (DLLs) it loads into a process. These show up in purple in Mnemonic, and the assembly name is shown in the status bar.
History
- 24 November 2011: First version
- 9 February 2012: Updated the code to run under both 32- and 64-bit Windows, and to support both 32- and 64-bit processes