MMPFN

The MMPFN (Memory Manager Page Frame Number) structure is the key to pretty much everything that the Memory Manager knows about a page of physical memory that is in general use. There can be, and typically is, other physical memory known to Windows but it is treated as device-specific memory in a so-called I/O space and is never used to support allocations of virtual memory: pages of memory in the I/O space are not represented by MMPFN structures.

The MMPFN is arguably the most fundamental of all Memory Manager structures. An array of MMPFN structures, one for each page from physical address zero to the highest possible, is one of the kernel’s largest single uses of memory, and certainly of memory that isn’t ever paged out. A little more than 1% of physical memory—a little less on 32-bit Windows—is lost from general use as overhead just from this MMPFN array.

To inspect either a single MMPFN or the whole array of them when debugging, use the !pfn command or get the array’s address from the internal variable named MmPfnDatabase. In 64-bit Windows, starting with Windows Vista, this variable is pre-set to FFFFFA80`00000000, which is hard-coded throughout the kernel as the one address of the MMPFN array. This stops for the 1607 release of Windows 10. Apparently as a continuing programme of kernel-mode Address Space Layout Randomization (ASLR), this address is among the several whose (many) hard-coded references throughout the kernel get changed at load time through the Dynamic Value Relocation Table in the kernel’s load configuration.

Variability

With one MMPFN for every physical page, nobody wants this structure to grow as Windows evolves. In all known Windows versions, the MMPFN is:

That the MMPFN doesn’t change in size certainly doesn’t mean that it doesn’t change. The MMPFN is one of the most complex structures in all of kernel-mode Windows programming and its layout has varied greatly as ever more gets packed in ever more intricately.

Inevitably, variation occurs even between builds of the one version. Along with the conventional dated names of the frequent Windows 10 releases, this note uses the following shorthands for variations within old versions:

Note that although Windows XP SP2 is much the more frequent cut-off for change in version 5.1, the MMPFN changed earlier.

Layout

Microsoft’s name for the MMPFN structure itself and the names and types of its members are known from type information in public symbol files, starting with Windows 2000 SP3. What’s shown for earlier versions is inferred from what use these are (yet) known to make of the MMPFN. Where known use corresponds closely with that of a version for which Microsoft’s symbols are available, it seems reasonable to suppose continuity—but this is more than usually uncertain for this structure, given its elaborate substructure. Some use anyway has no correspondence, the code having changed too much. Even where the use hasn’t changed, tracking it down exhaustively would be difficult, if not impossible, even with source code.

Offset (x86) Offset (x64) Definition Versions
0x00 0x00
union {
    /*  changing members, follow link  */
} u1;
3.10 to 6.3
union {
    LIST_ENTRY ListEntry;
    RTL_BALANCED_NODE TreeNode;
    struct {
        /*  see below: ListEntry Overlay  */
    };
};
10.0 and higher
0x04 (3.10 to 5.2) 0x08 (late 5.2)
MMPTE *PteAddress;
3.10 to 5.2
0x08 (3.10 to 5.2);
0x04 (6.0 to 6.3);
0x10
0x10 (late 5.2);
0x08 (6.0 to 6.3);
0x18
union {
    /*  changing members, follow link  */
} u2;
3.10 to 6.3
MIPFNBLINK u2;
10.0 and higher
0x08 (6.0 to 6.3) 0x10 (6.0 to 6.3)
union {
    /*  see below: Union with PteAddress  */
};
6.0 to 6.3

The MMPFN begins with u1 in all versions. What changes for u1 in version 10.0 is only that it used to be the whole first member of the MMPFN but is now wrapped in an unnamed structure in an unnamed union.

The PteAddress is also ancient. It is originally the second MMPFN member. Version 6.0 moved it to third and wrapped it in a union. Version 10.0 restores it to second but keeps it in the union—hidden a little, in that it is now in an unnamed union in an unnamed structure in an unnamed union.

An important point to u1 and u2 is that they provide forward and backward links, named Flink and Blink, to other physical pages on any of several lists, e.g., of free pages. Just as important, considering that the MMPFN must be kept small, is that a physical page is not always on any list and so the space for these links has other use in all versions. The union with the Blink has always been made prominent by the debugger: even the I386KD for version 3.10 reports the contents of u2 as “blink / share count”.

Offset (x86) Offset (x64) Definition Versions
0x0C (3.10 to 6.3);
0x14
 
USHORT ReferenceCount;
3.10 to 3.50
 
ULONG ReferenceCount;
3.51 only
0x18 (late 5.2 to 6.3);
0x20
union {
    /*  changing members, follow link  */
} u3;
4.0 and higher
0x0E (3.10 to 3.50)   unknown 16-bit count 3.10 to 3.50
  0x1C (late 5.2 to 6.3);
0x24
ULONG UsedPageTableEntries;
late 5.2 only
USHORT UsedPageTableEntries;
6.0 to 6.1
USHORT NodeBlinkLow;
6.2 and higher
  0x1E (6.0 to 6.3);
0x26
UCHAR VaType;
6.0 to 6.1
struct {
    UCHAR Unused : 4;
    UCHAR VaType : 4;
};
6.2 to 1607
struct {
    UCHAR Unused : 4;
    UCHAR Unused2 : 4;
};
1703 and higher
  0x1F (6.0 to 6.3);
0x27
UCHAR ViewCount;
6.0 to 6.1
union {
    UCHAR ViewCount;
    UCHAR NodeFlinkLow;
};
6.2 and higher

The MMPFN originally has two 16-bit counters at offsets 0x0C and 0x0E, respectively. Version 3.51 dropped one and widened the survivor to 32 bits. Version 4.0 narrowed it and squeezed in bit flags. Certainly by version 5.0, the combination is dressed into a third union, inside which there occur significant rearrangements, including that the ReferenceCount within the union is first at offset 0x0E but then moves back to offset 0x0C.

The unknown 16-bit count in versions before 3.51 is presented as “valid pte count” by the I386KD.EXE for version 3.10. A good guess at naming the member might be ValidPteCount, inferring a pattern from the other count’s presentation as “reference count” to match the name ReferenceCount that is known for later versions with the certainty of symbol files. Against this is the usual observation that the programmer who writes for the debugger need not, and often does not, reproduce from the definitions that apply to the programming. For instance, the same line of output from the debugger has merely “color” for the MMPFNENTRY field that is known to have the later name PageColor and then next line has “restore pte” for what is later known to be OriginalPte.

Because of the structure’s 8-byte alignment in 64-bit Windows, the 32-bit u3 is inevitably followed by space for four new bytes. This does eventually get put to use for something that has no real applicability to 32-bit Windows. In 64-bit Windows 8 and higher, an MMPFN can be on two lists concurrently. The 64-bit u1 and u2 have not just one Flink and Blink each but two. The second pair link pages that belong to the same NUMA node. But space is tight and so some bits of each link end up in these four bytes that are unique to 64-bit Windows.

Offset (x86) Offset (x64) Definition Versions
0x10 (3.10 to 6.3) 0x20 (late 5.2 to 6.3)
MMPTE OriginalPte;
3.10 to 5.1
union {
    MMPTE OriginalPte;
    LONG AweReferenceCount;
};
5.2 only
union {
    MMPTE OriginalPte;
    LONG volatile AweReferenceCount;
};
6.0 to 6.1
MMPTE OriginalPte;
6.2 to 6.3
0x14 (non-PAE);
0x18 (PAE)
0x28 unknown MMPFNENTRY 3.10 to 3.51
ULONG PteFrame;
4.0 to 5.0
union {
    /*  changing members, follow link  */
} u4;
5.1 and higher

The OriginalPte is the least disturbed of all MMPFN members. Though versions 5.1 to 6.2 have it in union with one other member, it keeps its position up to and including version 6.3. It doesn’t go away in version 10: it just moves forward into the unnamed structure in the unnamed union that now begins the MMPFN.

All versions have a PteFrame in the last member. It is the PFN of what the !pfn command names the “containing page”. Each MMPFN represents one page of physical memory. The Page Frame Number (PFN) of this page is not kept in the MMPFN since it is known from the position of the MMPFN in the MmPfnDatabase. However, when the page is addressable its PFN is necessarily in some Page Table Entry (PTE). In a simple mapping, one PTE that provides access to the page has its address in the page’s MMPFN as the PteAddress and its PFN as the PteFrame. Originally, this PteFrame is kept with bit flags in something like the pattern of a PTE which is here thought to be the original MMPFNENTRY. Version 4.0 moved the flags away, leaving the PteFrame unadorned. Pressure for more flags soon had new flags squeezed back in with PteFrame in the unnamed union that is u4.

ListEntry Overlay

Windows 10 introduces to the MMPFN the relatively large LIST_ENTRY and RTL_BALANCED_NODE structures. The former is two pointers. The latter is three, in effect. Three old members (that are each pointer-sized or larger) are made into an unnamed structure that shares the space. Take away the construction and the Windows 10 MMPFN has the following 0x10 or 0x18 bytes at its start:

Offset (x86) Offset (x64) Definition Versions
0x00 0x00
union {
    /*  changing members, follow link   */
} u1;
10.0 and higher
0x04 0x08
union {
    /*  see below: Union with PteAddress  */
};
10.0 and higher
0x08 0x10
MMPTE OriginalPte;
10.0 and higher

Union with PteAddress

The PteAddress is a direct MMPFN member before version 6.0, which wraps it in an unnamed union and shifts it deeper into the structure. Version 10.0 then wraps the whole of this union into an unnamed structure within a different unnamed union and brings it back towards the front.

Offset (x86) Offset (x64) Definition Versions
0x08 (6.0 to 6.3);
0x04
0x10 (6.0 to 6.3);
0x08
MMPTE *PteAddress;
6.0 and higher
PVOID volatile VolatilePteAddress;
6.0 and higher
LONG volatile Lock;
6.1 to 6.3
ULONG_PTR PteLong;
6.1 and higher

The Lock is just the low bit. It is operated much like a KSPIN_LOCK but it is not one. Notably, it does not have the instrumentation of a KSPIN_LOCK. Having it in union with the PteAddress may have seemed natural. It is acquired while setting the PteAddress, which should never itself have the low bit set. That said, version 10.0 reimplements it as the high bit in u2, with the Blink and ShareCount.