MMPTE_HARDWARE

The MMPTE_HARDWARE structure is Microsoft’s representation of a Page Table Entry (PTE) such as the processor can use when translating a linear address to a physical address.

The processor finds these PTEs by following a chain of physical addresses that begin from what’s in the cr3 register. The translation algorithm iterates through as many as four levels. Since this is all documented by Intel and our concern is less with the processor than the Memory Manager, a brief summary suffices here. Successively less significant parts of the linear address each provide an index into successively lower levels of page tables. These get successively more clumsy names—page directory, page directory pointer table and page map level 4 (PML4) table—but each page table at whatever level is a page-sized array of PTEs. The indexed PTE at each level provides the physical address of the next (lower) level of page table until the iteration completes. The physical address from the lowest-level PTE is the physical address of the page that contains the given linear address.

There are presently three translation algorithms. Their mechanisms differ in the size of the PTE and the maximum possible depth of the iteration. The effect is of translating between different sizes of linear and physical address spaces. The choice is made even before the kernel is loaded. Indeed, the kernel exists in different forms which each implement only one of the translation algorithms:

Algorithm PTE Size Depth Linear Address Width Physical Address Width Versions Kernels
x86 4 bytes 2 32 bits 32 bits 3.10 to 5.2 32-bit NTOSKRNL.EXE
32-bit NTKRNLMP.EXE
6.0 to 6.1 32-bit NTOSKRNL.EXE
PAE 8 bytes 3 32 bits 36 bits 5.0 to 5.2 32-bit NTKRNLPA.EXE
32-bit NTKRPAMP.EXE
6.0 to 6.1 32-bit NTKRNLPA.EXE
6.2 and higher 32-bit NTOSKRNL.EXE
x64 8 bytes 4 48 bits 48 bits late 5.2 64-bit NTOSKRNL.EXE
64-bit NTKRNLMP.EXE
6.0 and higher 64-bit NTOSKRNL.EXE

 

Access

The Memory Manager, of course, accesses PTEs by linear addresses, not physical. It greatly eases this work by preparing the highest-level page table to have one of its PTEs give the physical address of that same page table. This creates a linear address whose translation to a physical address uses just this one PTE at all levels, but its larger consequence is that the PTEs for successive pages in the whole linear address space become addressable as one array. Through most of the history of Windows, and still in 32-bit Windows, the base address of this PTE array is preset:

  Symbolic Name x86 PAE x64 (Before 1607)
First PTE PTE_BASE C0000000 C0000000 FFFFF680`00000000
First PDE PDE_BASE C0300000 C0600000 FFFFF6FB`40000000
First PDPTE PPE_BASE (x64)   C0603000 FFFFF6FB`7DA00000
First PML4E PXE_BASE (x64)     FFFFF6FB`7DBED000
Self-Mapping PTE PXE_SELFMAP (x64) C0300C00 C0603018 FFFFF6FB`7DBEDF68
Last byte of last PML4E PXE_TOP (x64)     FFFFF6FB`7DBEDFFF
Last byte of last PDPTE PPE_TOP (x64)   C060301F FFFFF6FB`7DBFFFFF
Last byte of last PDE PDE_TOP C0300FFF C0603FFF FFFFF6FB`7FFFFFFF
Last byte of last PTE PTE_TOP C03FFFFF C07FFFFF FFFFF6FF`FFFFFFFF

Microsoft’s macro definitions for the x64 values of these magic addresses have been published in the NTDDK.H and other headers from all versions of the Device Driver Kit (DDK) and Windows Driver Kit (WDK) from as far back as Windows XP. That Microsoft has matching definitions for the x86 values is no surprise but public confirmation is known only from the NTOSP.H file from the Enterprise WDK for the 1511 release of Windows 10. Starting with the 1607 release of Windows 10, the x64 kernel is still built with these addresses hard-coded, e.g., as immediate data in instructions, but in a continuing programme of Address Space Layout Randomization (ASLR) they all get changed at load time through the Dynamic Value Relocation Table in the kernel’s load configuration.

The reverse engineer or even the programmer who’s doing kernel-mode debugging will not get far into the Memory Manager’s code without encountering sequences that compute the address of the PTE for an arbitrary linear address. Let MmPteBase be an internal variable or the hard-coded PTE_BASE, but either way a pointer to the whole MMPTE array. Then the address of the PTE for the linear address p is

&MmPteBase [(ULONG_PTR) (p) >> PAGE_SHIFT]

in 32-bit Windows, with or without Physical Address Extension (PAE), but the computation for 64-bit Windows needs just a little more because only the low 48 bits of a 64-bit linear address are meaningful:

&MmPteBase [((ULONG_PTR) (p) & 0x0000FFFFFFFFFFFF) >> PAGE_SHIFT]

See that the computation is recursive. For instance, the address of the PDE for a given address is the address of the PTE for the PTE for that address. This recursion of course produces more formulae that the programmer or reverse engineer may do well to recognise. For the magic addresses in particular, the recursion is that the PTE for PTE_BASE is at PDE_BASE, the PTE for which is at PPE_BASE, and so on, as far as applicable to the architecture. The PTE for the self-mapping PTE is itself.

Layout

That a PTE is intended for the processor’s interpretation is presumably what makes it a hardware PTE for the Memory Manager. In Intel’s terminology for the processor, the translation from linear address to physical address can complete only if each PTE along the way has a set P bit (masked by 0x01). Only then is anything else in a PTE meaningful to the processor. Encountering a hardware PTE with a clear P bit causes the processor to raise an exception. The effect is to hand the PTE to software for interpretation as if the PTE were instead a software PTE. It is here thought that the MMPTE_HARDWARE is the Memory Manager’s one structure for a PTE that is intended to be interpreted by the processor. An MMPTE_HARDWARE may have a clear P bit but its continued interpretation is then as an MMPTE_SOFTWARE.

Names and types in the following tables are from public symbol files for the kernel, starting with Windows 2000 SP3 in general. The exception is that symbol files for the single-processor kernel without PAE support don’t have type information before Windows XP. This mostly doesn’t matter and is left unspecified almost everywhere that this website says that information is from public symbol files for Windows 2000, but it matters for the MMPTE_HARDWARE because the kernel manages some bits differently if it has the complication of executing on multiple processors.

32-Bit PTE

For the x86 builds that do not use PAE, page table entries are four bytes. The whole MMPTE_HARDWARE is a structure of ULONG bit fields:

Mask Definition Versions Remarks
0x00000001
ULONG Valid : 1;
all Intel’s P;
must be set for processor to interpret any other bits
0x00000002
ULONG Write : 1;
3.10 to 3.51;
4.0 to 5.2 (UP)
Intel’s R/W
ULONG Writable : 1;
4.0 to 5.2 (MP)  
ULONG Dirty1 : 1;
6.0 and higher  
0x00000004
ULONG Owner : 1;
all Intel’s U/S
0x00000008
ULONG WriteThrough : 1;
all Intel’s PWT
0x00000010
ULONG CacheDisable : 1;
all Intel’s PCD
0x00000020
ULONG Accessed : 1;
all Intel’s A
0x00000040
ULONG Dirty : 1;
all Intel’s D
0x00000080
ULONG LargePage : 1;
all Intel’s PAT or PS
0x00000100
ULONG Global : 1;
all Intel’s G
0x00000200
ULONG CopyOnWrite : 1;
all  
0x00000400
ULONG Prototype : 1;
all  
0x00000800
ULONG reserved : 1;
3.10 to 3.51;
4.0 to 5.2 (UP)
 
ULONG Write : 1;
4.0 to 5.0 (MP)  
0xFFFFF000
ULONG PageFrameNumber : 20;
all  

Remember that “all” and “higher” reach only to version 6.1. Though kernels without support for PAE may exist in theory for version 6.2 and higher, none are known to have been distributed.

Given that Valid is set, the lowest nine bits all have their meaning defined by Intel for interpretation by the processor. The CopyOnWrite, Prototype and (the multi-processor) Write bits are how Windows uses the three bits that Intel leaves to the operating system even in a hardware PTE.

The CopyOnWrite bit acts most usefully for a page that is not enabled for Write access. The physical page that corresponds to the linear address is protected from change. Attempting to write to it causes a page fault which the operating system resolves by finding a new physical page, filling it with the contents of the protected page, and then remapping the linear address to the new page.

The Prototype bit has an important role in the various types of software PTE. In general, an MMPTE_SOFTWARE that has the Prototype bit set is interpreted next as an MMPTE_PROTOTYPE. Early versions go straight to this interpretation even without testing that Valid is clear.

Redefining the Write bit from Intel’s 0x00000002 to 0x00000800 is Microsoft’s way around a problem that multi-processor systems present to operating-system software that would clear the Dirty bit. This latter is set in the PTE by any processor that writes to any linear address whose translation ends with this PTE. That the processor does this is vital for the Memory Manager’s tracking of which pagable pages, i.e., pages of linear address space whose contents are subject to being paged in and out between physical memory and disk storage, have been modified. When the Memory Manager acts on a set Dirty bit, it clears not just this bit but also the Writable bit so that whatever it aims to do about the modified page cannot be interfered with by other processors (which temporarily see the page as having no write access). Meanwhile, the Write bit is the Memory Manager’s record of whether the page is eventually to have its write access restored. Windows Vista, which discontinues the single-processor kernels, formalises that the Dirty and Writable bits (the latter now named Dirty1) are cleared together.

64-Bit PTE

For 64-Bit Windows but also for the x86 builds that use PAE, page table entries are eight bytes and the MMPTE_HARDWARE is a structure of ULONGLONG bit fields. The low 12 bits of the 64-bit PTE, whether for PAE and x64, match closely those of the 32-bit PTE:

Mask Definition Versions Remarks
0x00000000`00000001
ULONGLONG Valid : 1;
all Intel’s P;
must be set for processor to interpret any other bits
0x00000000`00000002
ULONGLONG Write : 1;
5.0 to 5.2 (UP) Intel’s R/W
ULONGLONG Writable : 1;
5.0 to 5.2 (MP)  
ULONGLONG Dirty1 : 1;
6.0 and higher  
0x00000000`00000004
ULONGLONG Owner : 1;
all Intel’s U/S
0x00000000`00000008
ULONGLONG WriteThrough : 1;
all Intel’s PWT
0x00000000`00000010
ULONGLONG CacheDisable : 1;
all Intel’s PCD
0x00000000`00000020
ULONGLONG Accessed : 1;
all Intel’s A
0x00000000`00000040
ULONGLONG Dirty : 1;
all Intel’s D
0x00000000`00000080
ULONGLONG LargePage : 1;
all Intel’s PAT or PS
0x00000000`00000100
ULONGLONG Global : 1;
all Intel’s G
0x00000000`00000200
ULONGLONG CopyOnWrite : 1;
all  
0x00000000`00000400
ULONGLONG Prototype : 1;
5.0 to 6.0  
ULONGLONG Unused : 1;
6.1 and higher  
0x00000000`00000800
ULONGLONG reserved0 : 1;
5.0 to 5.2 (UP)  
ULONGLONG Write : 1;
all (MP)  

Remember that “all” for PAE support begins with version 5.0. Similarly, the first x64 build is version 5.2 from Windows Server 2003 SP1.

For these low bits, the only difference from the 32-bit structure (for bits that aren’t reserved) is that version 6.1 removed Prototype from one but not the other. The remaining bits differ significantly, not just from the 32-bit PTE but between the PAE and x64 implementations:

Mask (PAE) Definition Versions
0x0000000F`FFFFF000 (5.0);
0x0000003F`FFFFF000
ULONGLONG PageFrameNumber : 24;
5.0 only
ULONGLONG PageFrameNumber : 26;
5.1 and higher
 
ULONGLONG reserved1 : 28;
5.0 only
ULONGLONG reserved1 : 26;
5.1 to 1607
ULONGLONG reserved1 : 25;
1703 and higher
0x80000000`00000000
ULONGLONG NoExecute : 1;
1703 and higher

In the first PAE kernels, the PageFrameNumber can describe 16M pages, as if for 36 address lines and 64GB of physical memory. Version 5.1 raises this to 64M pages, as if for 38 address lines. This would allow 256GB of physical memory, even though 32-bit Windows cannot possibly support so much. (It has a long-standing architectural limit of 128GB caused by needing kernel-mode address space for an array of MMPFN structures, one per page of physical memory. At 0x1C bytes per MMPFN, even 128GB of physical memory requires 896MB for the MMPFN array when at most 1GB can be available.)

It is not known why NoExecute is not defined for the PAE builds until a later Windows 10 release. The x64 builds have it from the start.

Mask (x64) Definition Versions
0x000000FF`FFFFF000 (5.2 to early 6.0);
0x0000FFFF`FFFFF000
ULONGLONG PageFrameNumber : 28;
late 5.2 to early 6.0
ULONGLONG PageFrameNumber : 36;
late 6.0 and higher
 
ULONGLONG reserved1 : 12;
late 5.2 to early 6.0
ULONGLONG reserved1 : 4;
late 6.0 to 1607
ULONGLONG ReservedForHardware : 4;
1703 and higher
0x7FF00000`00000000 (late 5.2 to 1607)
ULONGLONG SoftwareWsIndex : 11;
late 5.2 to 1607
 
ULONGLONG ReservedForSoftware : 4;
1703 and higher
0x0F000000`00000000
ULONGLONG WsleAge : 4;
1703 and higher
0x70000000`00000000
ULONGLONG WsleProtection : 3;
1703 and higher
0x80000000`00000000
ULONGLONG NoExecute : 1;
all (x64)

The first 64-bit kernels provide for 256M pages, as if for 40 address lines and 1TB of physical memory. This was raised for the version 6.0 from Windows Vista SP1. The widened PageFrameNumber allows 48 address lines and thus 256TB of physical memory. Note that the PageFrameNumber in the otherwise very close HARDWARE_PTE does not get this same widening until the version 6.1 from Windows 7 SP1. It is not known whether the lag in updating the HARDWARE_PTE had real-world consequence.

In the high dword, 64-bit Windows defines SoftwareWsIndex as using all 11 bits that Intel leaves as available if the processor is not using protection keys.

MMPTE_HARDWARE_LARGEPAGE

The MMPTE_HARDWARE leaves unspecified that not all the bits of the PageFrameNumber are meaningful in a PTE for a large page. In a PTE for a 2MB page—or, if you prefer, a PDE for which LargePage is set—the lowest bit of the PageFrameNumber is Intel’s PAT and the next eight are reserved, i.e., must be zero. This applies also in 64-bit Windows to a PTE for a 1GB page, i.e., a PDPTE for which LargePage is set, except that nine more bits are reserved.

Early versions of the 64-bit kernel define an MMPTE_HARDWARE_LARGEPAGE to model this for 2MB pages:

Mask (x64) Definition Versions
0x00000000`00001000 (late 5.2 to 6.0)
ULONGLONG PAT : 1;
late 5.2 to 6.0
 
ULONGLONG reserved1 : 8;
late 5.2 to 6.0
0x000000FF`FFE00000 (5.2 to early 6.0);
0x0000FFFF`FFE00000
ULONGLONG PageFrameNumber : 19;
late 5.2 to early 6.0
ULONGLONG PageFrameNumber : 27;
late 6.0 only
0xFFFFFF00`00000000 (5.2 to early 6.0);
0xFFFF0000`00000000
ULONGLONG reserved2 : 24;
late 5.2 to early 6.0
ULONGLONG reserved2 : 16;
late 6.0

This was discontinued for version 6.1, perhaps because it’s more trouble than it’s worth. It captures that the page frame number loses its low nine bits but it leaves the PageFrameNumber no longer equal to the page frame number.