Process Memory Map in Code: Mapped Files and DOS Paths (Part 2)

Author

Pavel Yosifovich has 25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.

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 1 walked a process’s address space with VirtualQueryEx and located the Process Environment Block. This time the tool learns to identify what file backs a mapped or image block, which surfaces a Windows quirk most developers never have to think about: the path Windows hands back is not the one you typed into Explorer.

Why Cache the PEB Address Instead of Querying It Every Time?

Before adding anything new, Pavel cleans up something wasteful left over from Part 1: the tool was calling NtQueryInformationProcess to locate the Process Environment Block every single time it examined a block, even though the PEB address for a given process never changes. A simple fix is a static local variable that holds the cached address. The first time through, if it is still unset, the tool queries it and stores the result; every call after that reuses the cached value instead of repeating the same syscall for no reason. It is a small change, and it does not alter the output at all, but it is the kind of inefficiency Pavel says he likes to catch before moving on to new functionality.

How Do You Get the File Backing a Mapped or Image Block?

VMMap shows the actual file behind any block of type Image or Mapped, and getting that in code turns out to be one function call: GetMappedFileName. It lives in psapi.h, the Process Status API header, which needs its own separate include. Called with the process handle, the block’s base address, and a buffer sized to MAX_PATH, it returns the path of the file mapped at that address, or zero if there is no file backing the block, which is the tool’s cue to just return early for anything that is not MEM_IMAGE or MEM_MAPPED. Running it against Explorer immediately shows the mapped data files. Images take slightly more interpretation: VMMap goes a step further and parses the PE headers of an image block to break it into named sections, something this tool does not attempt, but the read-only header block followed by a read-execute block is already a strong hint that a given region is an image, not just mapped data.

Why Does the Path Start with \Device\HarddiskVolume3 Instead of C:\?

The first path GetMappedFileName returns is not what most Windows developers expect: something like \Device\HarddiskVolume3\Windows\explorer.exe instead of C:\Windows\explorer.exe. This is an NT path, rooted in the object manager namespace that the Windows kernel actually uses internally, and C: is not fundamental to that namespace at all. It is a symbolic link, visible in WinObj under the \?? directory, that points at the real device object, \Device\HarddiskVolume3 on this system. VMMap resolves that link and shows the familiar drive-letter path instead, which is the more readable format Pavel wants his own tool to match.

How Do You Convert an NT Path to a DOS Path?

Doing the same resolution means walking the mapping in the other direction: given \Device\HarddiskVolume3\..., find which drive letter points there. GetLogicalDrives returns a bitmask of every drive letter currently mounted on the system, one bit per letter starting from A. Pavel walks that bitmask a bit at a time, and for every drive that exists, calls QueryDosDevice with the drive letter to get back the real device path it resolves to, then stores both directions in a std::map keyed by the device path. Like the PEB address, this mapping is calculated once and cached, since it will not change while the tool runs.

With the map built, converting a path is a matter of finding where the device name ends and the actual file path begins. Every NT path in this map starts with two backslashes before the volume name and a third backslash marking the start of the real path, so strchr searching from a fixed offset finds that boundary. The substring before it is the device prefix; looking that prefix up in the map returns the matching drive letter, which gets concatenated with everything from the third backslash onward. Once that logic is right, the tool’s output finally reads C:\Windows\... the way VMMap shows it.

What Went Wrong Along the Way?

Getting there was not a straight line. The first bug showed up in the drive-letter loop itself: an off-by-one in how the drive letter character was assembled meant the map was built with the wrong keys until Pavel corrected the offset and reran it against his own C, D, and E drives. The second bug hit in the lookup itself, where the initial code checked the wrong iterator against the map’s end and reported a lookup failure even though the entry was actually there. Both were the ordinary kind of typo that shows up when you write this much pointer-and-index code in one sitting, and both were fixed by breaking into the debugger and stepping through the exact values rather than guessing.

What This Means Practically

  • Cache anything that queries fixed process state, like the PEB address, in a static variable instead of re-querying it on every iteration
  • Use GetMappedFileName from psapi.h to identify the file backing an image or mapped memory block
  • Expect NT paths like \Device\HarddiskVolumeN\... from low-level APIs; C:\ drive letters are symbolic links in the object manager namespace, not the real path
  • Build a device-path-to-drive-letter map once with GetLogicalDrives and QueryDosDevice, and reuse it, since drive mappings do not change mid-run
  • When a lookup unexpectedly fails, check the iterator comparison against the container’s end() before assuming the data itself is wrong

Part 3 picks up with Thread Environment Blocks: getting there means enumerating every thread in the process.

Keep Learning

If you want to go further with the Native API and virtual memory internals behind this tool, these TrainSec courses go deeper:

Related reading in the knowledge library: Process Memory Map in Code, Part 1, which builds the VirtualQueryEx walk and finds the PEB, and VMMap Basics, the GUI-first walkthrough this series builds toward matching.

blue depth

About the author

Pavel Yosifovich has 25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.