Pavel Yosifovich, co-author of “Windows Internals 7th Edition, Part 1” and author of “Windows Native API Programming”, continues his series building a code version of VMMap. Part 3 taught the tool about thread stacks and Thread Environment Blocks. The obvious gap left is images. VMMap does not just say “this range belongs to ntdll.dll”, it breaks each mapped module down into its sections: the header page, .text, .rdata, .data and the rest. This part makes the tool do the same thing, and then explains why the result still does not fully agree with VMMap.
How Do You Tell an Image Block From a Plain Mapped File?
The tool already classifies each block by memory type. Anything that is not MEM_PRIVATE is either mapped or an image, and both cases so far went through the same path: call GetMappedFileName and convert the NT device path into a DOS path with drive letters, the machinery built back in Part 2. That is where the split has to happen. If MEMORY_BASIC_INFORMATION.Type is MEM_MAPPED, this is just mapped data, not a PE, or at least not mapped as one, so the file name is the whole story and there is nothing more to say about it. If it is not private and not mapped, the only remaining option is MEM_IMAGE, and an image has sections. So for that case the tool prints the path, then appends the name of whatever section the block lands in, from a new helper called GetPESectionName.
How Do You Read a PE Header Out of Another Process?
Every block that GetPESectionName gets comes with two pointers worth caring about: BaseAddress, the block being considered right now, and AllocationBase, the start of the whole allocation, which for an image is exactly where the PE header sits. So there is a free special case up front: if the two are equal, this block is the header, and the function can return the string “header” without parsing anything at all.
For every other block the header actually has to be read. A 4KB buffer is enough, because the header never spills past a single page and is usually much smaller. The header lives in another process, so getting it means ReadProcessMemory at AllocationBase, which in turn means the function needs the process handle passed in alongside the MEMORY_BASIC_INFORMATION, the same way the other block detail helpers in this tool already take it. That read can genuinely fail, and not because of a bug: the target process might unload that DLL in the moment between the block enumeration and the read. Nothing to be done about that, so the function returns an empty string and moves on.
How Do You Get From the DOS Header to the Section Headers?
A PE starts with an IMAGE_DOS_HEADER, which exists purely for compatibility reasons but is part of the format nonetheless. Its e_lfanew member is the offset to the “real” headers, so casting the buffer to a DOS header, reading e_lfanew, and adding it to the byte buffer gets you to IMAGE_NT_HEADERS. There is a shortcut worth knowing: ImageNtHeader does the same arithmetic for you. It is declared in dbghelp.h and needs dbghelp.lib linked in, which is the only reason not to reach for it by default.
From the NT headers, IMAGE_FILE_HEADER.NumberOfSections gives the loop bound. Finding the first section header means skipping past the optional header, whose size is not fixed, and the IMAGE_FIRST_SECTION macro in winnt.h does exactly that: it takes the address of the optional header and advances by SizeOfOptionalHeader. The macro returns a pointer to the first IMAGE_SECTION_HEADER, the descriptor of the section rather than the section itself, and because those descriptors are contiguous, walking the rest is just incrementing the pointer.
How Do You Match a Block Address to a Section?
For each section descriptor, the tool computes where that section actually starts in memory. The VirtualAddress member is the source of confusion here, because it is not a virtual address. It is a relative virtual address, an RVA, an offset from the module’s base. So the real start is AllocationBase + VirtualAddress, and the block belongs to that section when BaseAddress is at or after the start and before start + VirtualSize.
VirtualSize is the right size field to use, and this matters. The section header carries a couple of size-looking members, and SizeOfRawData is the one that applies when the file is mapped as plain data. When the file is mapped as an image, which is exactly the case here, VirtualSize is the correct field.
This is also where the live bug in the video shows up. The first version added VirtualAddress to BaseAddress, the address currently being examined, instead of to AllocationBase, the module base the RVA is relative to. The result was section names that never matched anything. Setting a breakpoint in the helper, confirming that the buffer really did start with the MZ signature, and seeing the first section come back correctly as .text narrowed it down to the comparison itself. Worth noting the smaller mistake that came first: no output at all, because the wrong binary was being run.
$1,938
$1,356 or $140 X 10 payments
Windows Internals Master
Broadens and deepens your understanding of the inner workings of Windows.
Why Is a Section Name Not Necessarily Null Terminated?
IMAGE_SECTION_HEADER.Name is an array of eight bytes. Not char, which is a little odd, but that is what it is. The trap is that eight bytes is the maximum, not the maximum plus a terminator. A section whose name happens to be exactly eight characters has no null at the end, so casting the array to const char* and letting the std::string constructor find the terminator reads past the field. The fix is to build the string from an explicit range, which handles the eight-character case correctly and still works for shorter names, since those are zero padded. Once a matching section is found the function returns immediately, and if the loop finishes without a hit the block belongs to no section and the result is an empty string.
Why Doesn’t the Output Match VMMap?
With the bug fixed, section names appear and they look right. Compare a specific module against VMMap, though, and the two views diverge. For rasapi32.dll, VMMap shows .pdata, .didat, .rsrc and the relocation section as separate entries. The tool shows .pdata, then a blank. The sizes still add up: VMMap has a 32KB span broken into a 20KB chunk plus three 4KB chunks, while the tool reports one block.
The cause is VirtualQueryEx itself. It reports a run of pages as a single block whenever they share the same protection and memory type. Those four sections are all read only, so as far as VirtualQueryEx is concerned they are one region, and the tool, which builds its entire view out of the blocks that function returns, can never see the boundary. VMMap does not have that limitation because it does not rely on VirtualQueryEx alone: it parses the section table, maps the sections back onto memory, and splits the block list to accommodate them. Doing the same here means special-casing every image and generating entries from the section table rather than from VirtualQueryEx output, a significant restructuring of the block iteration that Pavel leaves as an exercise. Parsing the data directories and mapping them into their sections is a natural next exercise for anyone who wants to get more comfortable with the PE format.
What Is Still Missing Compared to VMMap?
Three gaps remain after this part. The biggest is heaps: the tool shows nothing about them. Two documented approaches are available. The tool help APIs enumerate a process’s heaps, via Heap32ListFirst and friends on a CreateToolhelp32Snapshot handle, following the pattern the series already used for threads. Alternatively the PEB, which the tool has located since Part 1, holds a ProcessHeaps array of heap addresses that can be used as a starting point for digging with VirtualQueryEx. There is a third option too, but either of these is enough to get started.
Managed heaps, meaning anything .NET specific, are out of scope for this tool. The last gap is the working set: VMMap shows which parts of a region are currently resident in RAM and which are not. VirtualQueryEx has no idea. It knows committed, reserved and free, and nothing about residency. QueryWorkingSetEx, from the same process status API family as GetMappedFileName, is the function that answers that questionץ
What This Means Practically
- Branch on
MEMORY_BASIC_INFORMATION.Typebefore doing PE work:MEM_MAPPEDis plain data with nothing to parse, onlyMEM_IMAGEhas a section table worth reading - Read a remote module’s header with
ReadProcessMemoryatAllocationBase, cap the buffer at 4KB, and treat a failed read as a module that unloaded rather than as a bug - Use
ImageNtHeaderfromdbghelpinstead of hand-casting throughe_lfanew, andIMAGE_FIRST_SECTIONinstead of addingSizeOfOptionalHeaderyourself - Compute a section’s start as
AllocationBase + VirtualAddress, sinceVirtualAddressis an RVA, and bound it withVirtualSize, notSizeOfRawData, when the file is mapped as an image - Do not expect
VirtualQueryExto expose section boundaries: it merges adjacent pages that share protection and type, so matching VMMap means driving the display from the section table instead - Reach for
QueryWorkingSetExwhen you need residency information, and the tool help heap APIs or the PEBProcessHeapsarray when you need heaps
Keep Learning
If you want to go deeper on the PE format, the Native API, and the virtual memory internals this tool is built on, these TrainSec courses cover the ground in full:
- Windows Native API Programming: the native API surface behind
NtQueryInformationProcess, the PEB and TEB, and the structures this series keeps reaching for - Windows System Programming 2: the virtual memory, process and thread APIs the tool is built on
Related reading in the knowledge library: Process Memory Map in Code, Part 1, Part 2 and Part 3, plus VMMap Basics, the GUI walkthrough this series has been working toward matching.