Every process on Windows runs with the comforting illusion that it owns a private, contiguous block of memory that stretches from address zero upward — regardless of how much physical RAM is actually installed or what other processes are doing. This illusion is virtual memory, and maintaining it is one of the most important jobs the Windows memory manager performs. Each memory access your code makes uses a virtual address that the CPU’s Memory Management Unit (MMU), working from tables the kernel builds, translates into a physical address on the fly. On x64, the virtual address space is split in two: the lower half (roughly 0x0 through 0x00007FFFFFFFFFFF) is user space, private to each process, and the upper half is kernel space, shared across all processes and only reachable from ring 0. Understanding how that translation works — and the data structures behind it — is the key to understanding memory protection, paging, and a large slice of modern exploitation.
Virtual to Physical Translation
Standard x64 systems use 4-level paging. A canonical 48-bit virtual address is carved into five fields that the MMU walks like a tree: 9 + 9 + 9 + 9 + 12 bits. The four 9-bit fields each index a 512-entry table, and the final 12 bits are the byte offset into a 4KB page. The walk proceeds through four levels:
- PML4 (Page Map Level 4) — the top-level table; its physical address lives in the CPU’s
CR3register. - PDPT (Page Directory Pointer Table) — selected by the next 9 bits.
- PD (Page Directory).
- PT (Page Table) — its entry finally points at the physical page frame.
The register CR3 is the linchpin of process isolation. It holds the physical address of the current process’s PML4, and Windows stores that value in each process’s kernel object as EPROCESS.DirectoryTableBase. When the scheduler performs a context switch to a thread in a different process, it reloads CR3 with that process’s DirectoryTableBase. Because a different top-level table means an entirely different set of translations, one process simply has no way to name — let alone touch — another process’s private memory. Isolation is not enforced by checking every access against a list; it is a structural consequence of swapping the page-table root.
The MMU caches recent translations in the Translation Lookaside Buffer (TLB) so it does not have to walk four tables on every access. Windows can also map memory using large pages — a translation that terminates early at the PD level yields a 2MB page, and one that terminates at the PDPT level yields a 1GB page. Large pages reduce TLB pressure and page-table overhead, which is why they are used for things like the kernel image and some database workloads.
The Page Table Entry (PTE)
Each entry in the final page table is a 64-bit Page Table Entry (PTE). When a page is resident in RAM, this is a hardware PTE whose format is defined by the CPU. It packs the Page Frame Number (PFN) — the physical page’s index — together with a set of control bits the MMU checks on every access:
- Present/Valid (P) — the page is in physical memory. If clear, any access triggers a page fault.
- Read/Write (R/W) — if clear, the page is read-only and a write faults.
- User/Supervisor (U/S) — if clear, only ring 0 may touch the page; user-mode access faults. This is what keeps kernel space unreachable from user mode.
- Execute-Disable (NX/XD) — bit 63; if set, instruction fetches from the page fault. This bit is the hardware foundation of Data Execution Prevention (DEP).
The interesting case is when the Present bit is clear. The page is not in RAM, so the CPU no longer cares about the rest of the bits — which frees Windows to reinterpret them. In this software PTE (also called an invalid PTE), the memory manager encodes where the page really is and how to get it back. Different software-PTE formats describe a page that is sitting in the paging file, one that is on a standby/modified list and can be reclaimed cheaply (a transition PTE), one that should be filled with zeros on first touch (demand-zero), or one that points at a shared prototype PTE for memory-mapped/shared pages. The single 64-bit slot is thus doing double duty: a hardware descriptor when valid, a bookkeeping record when not.
Page States: Valid, Transition, Standby, Modified
Every physical page of RAM is tracked by an entry in the PFN database (an array of MMPFN structures indexed by page frame number). The PFN entry records what the page is being used for and which state list it belongs to. The states that matter most are:
- Valid/Active — the page is mapped into some working set and in active use.
- Transition — temporarily in flux (for example, an I/O in progress).
- Standby — removed from a working set but still holds valid, clean data. It can be handed back to its owner via a cheap soft fault, or repurposed if memory is needed. The standby list is effectively Windows’ page cache.
- Modified (dirty) — removed from a working set but contains changes not yet written to disk. The modified page writer flushes these to the paging file (or the mapped file), after which they become standby.
When the system trims pages out of a process (see below), they do not vanish — they migrate to the standby or modified list first. This is why a page you “lost” to trimming can often be reclaimed without any disk I/O: it is still sitting in RAM on a list, one soft fault away from being valid again.
Working Sets
A process’s working set is the subset of its virtual pages that are currently resident in physical memory. Windows does not keep every committed page in RAM at once; it keeps the pages a process is actively using and lets the rest live in the paging file or backing files. The working-set manager, driven by the balance set manager, monitors memory pressure and trims working sets when free memory runs low, pushing least-recently-used pages onto the standby and modified lists.
The cost of getting a page back depends on where it went. A soft (minor) page fault is resolved entirely in memory — the page was on the standby or modified list, or the fault is demand-zero, so no disk access is needed. A hard (major) page fault requires reading the page from the paging file or a mapped image on disk, which is orders of magnitude slower. High hard-fault rates are the classic signature of a system that is thrashing under memory pressure.
Virtual Address Descriptors (VADs)
Page tables describe memory at the granularity of individual pages, but the memory manager also needs a higher-level view of what regions a process has asked for. That view is the VAD tree — a self-balancing binary tree of Virtual Address Descriptors, rooted at EPROCESS.VadRoot. Each VAD describes a contiguous range of the process address space: its start and end, its protection, and whether it is backed by a file (an image or data mapping) or is private memory.
This structure enables lazy memory management, which hinges on the distinction between reserve and commit. Reserving an address range (MEM_RESERVE) merely creates a VAD marking the range as taken — no physical memory or pagefile space is charged, and no PTEs are built. Committing (MEM_COMMIT) promises backing store for the pages, but even then the pages are typically demand-zero: no physical frame is assigned until the first access. When a thread finally touches such an address, the fault handler walks the VAD tree, finds the descriptor covering the faulting address, learns how the page should be materialized (zeroed, read from a file, copied on write), assigns a frame, and fills in a valid PTE. Only then does the access complete. No VAD, no valid backing — the result is the familiar access violation.
Memory Protections and DEP/ASLR
Page-level protection is where the memory manager becomes a security control. Each committed region carries a protection that maps onto the PTE control bits — constants such as PAGE_READONLY, PAGE_READWRITE, PAGE_EXECUTE_READ, and the notorious PAGE_EXECUTE_READWRITE (RWX). Data Execution Prevention (DEP) is simply the NX bit put to work: data regions like the stack and heap are mapped non-executable, so an attacker who writes shellcode into them cannot jump to it — the instruction fetch faults.
Address Space Layout Randomization (ASLR) attacks the other half of exploitation: knowing where things are. ASLR randomizes the base addresses of images (EXEs and DLLs), the stack, the heap, and other regions each boot or each process launch, so hardcoded addresses in an exploit no longer land where the attacker expects. From a defender’s standpoint, a region that is both writable and executable (RWX private memory) is a strong hunting signal — legitimate code is normally executed from image-backed, non-writable pages, so a large RWX private region often means code was written and then run there.
Inspecting Memory in WinDbg
All of these structures are directly observable in a kernel debugger. A handful of commands lets you follow a virtual address all the way down to a physical frame and back up to the region that owns it:
# Decode the paging hierarchy for a virtual address:
# shows the PML4E/PDPTE/PDE/PTE and the resulting physical frame,
# plus the protection and state (valid, transition, pagefile...).
!pte 0x000001a2`b3c40000
# Describe what a virtual address is: region type, protection,
# and the backing (image, mapped file, private, or free).
!address 0x000001a2`b3c40000
# Walk a process's VAD tree from EPROCESS.VadRoot to list every
# reserved/committed region, its protection, and its backing.
!vad ffffca80`12345678
# Inspect a single PFN database entry for a physical frame:
# its state list, reference count, and owning PTE.
!pfn 0x1f4a2
# Show a value in every radix/format — handy for hand-decoding
# a raw PTE value into its bit fields.
.formats 0x8000000012345867
!pte is the workhorse: it tells you at a glance whether a page is valid, what its protection is, and — if invalid — whether it is in transition, in the pagefile, or demand-zero. !vad gives you the region-level picture that !pte lacks, and !pfn lets you cross-reference a physical frame back to its owner.
Security Relevance
Nearly every step of modern exploitation is a negotiation with the memory manager’s protections. Because DEP marks data pages non-executable, attackers can no longer simply drop shellcode on the stack and run it; they are forced into Return-Oriented Programming (ROP), chaining existing executable gadgets to, for example, call VirtualProtect and flip a region to executable first. ASLR forces an information leak to defeat randomization before a payload can use absolute addresses. Each mitigation raises the bar and shapes the technique.
The same structures are a gift to defenders. Freshly allocated, private RWX memory is a high-fidelity indicator of code injection, because benign code executes from image-backed pages. And because the memory manager’s VAD tree is the ground truth of what is mapped — independent of the user-mode loader’s module list — walking the VADs reveals manually mapped or hidden modules that never registered with the loader. Memory-forensics tooling leans on exactly this: comparing the VAD/PFN view against the advertised module list to surface what an attacker tried to hide.
Conclusion
Windows virtual memory is a layered system: 4-level page tables and the MMU turn virtual addresses into physical ones, PTEs encode both hardware translations and software bookkeeping, the PFN database and working-set manager juggle scarce physical frames across state lists, and the VAD tree records each process’s intent at the region level. Together they deliver per-process isolation, lazy allocation, and the page-level protections that DEP and ASLR build on. With this foundation in place, we can look at how the kernel names and secures the resources a process uses — next in the series, the Object Manager and the handle table.
You Might Also Like
This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:



Comments