Geoff Chappell, Software Analyst
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 |
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.
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.
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.
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.
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.