MMPFN

The MMPFN structure (formally _MMPFN), its name apparently standing for Memory Manager Page Frame Number, 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. Though it is only small, the MMPFN is one of the most complex structures in all of kernel-mode Windows programming. 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 year-and-month convention for naming 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 other kernel-mode changes 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 nested 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 moves it back to offsets 0x04 and 0x08 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” (without revealing the name u2 or, indeed, MMPFN).

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

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 (for which Microsoft’s definition is known from public symbol files), the combination is dressed into a third union, inside which there then occur significant rearrangements, including that the ReferenceCount within the union is first the high 16 bits of the union but then moves back to the low 16 bits.

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 therefore 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 bit field that is known to have the later name PageColor and the next line of this output has “restore pte” for what is later known to be OriginalPte.

Offset (x64) Definition Versions
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 to 1903
union {
    UCHAR ViewCount;
    UCHAR NodeFlinkLow;
    UCHAR ModifiedListBucketIndex : 4;
};
2004 and higher

In 64-bit Windows, 8-byte alignment of most of the pre-existing members left the 32-bit u3 to be 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 are squeezed into u1 and u2 (as NodeFlinkHigh and NodeBlinkHigh) and others (as NodeFlinkLow and NodeBlinkLow) end up in these four bytes that are unique to 64-bit Windows.

The VaType which was introduced for 64-bit Windows Vista records that the physical page is dedicated to a specifc region of system virtual address space. Its values come from what was then the new MI_SYSTEM_VA_TYPE enumeration. Only in the 1709 release of Windows 10 has this needed more than four bits, but before then the rethinking for ASLR looks to have made it undesirable to keep this tight relation of physical page to virtual page. The VaType has gone and no use has since been found for the space.

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

Though versions 5.1 to 6.2 have the OriginalPte in union with one other member, it keeps its position within the MMPFN all the way from version 3.10 to 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 Page Frame Number (PFN) of what the !pfn command names the “containing page”. Each MMPFN represents one page of physical memory. To keep this page’s PFN in the MMPFN would be wasteful 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 to offset 0x0C, 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:

MMPFN Offset (x86) MMPFN 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 originally a direct MMPFN member. Version 6.0 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. Again, disregard the scaffolding and look just at the PteAddress is its unnamed union:

MMPFN Offset (x86) MMPFN 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 to 1803
LONG volatile Lock;
6.1 to 6.3
ULONG_PTR PteLong;
6.1 and higher

Though the Lock is formally a whole integer, it exists just for 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. Natural or not, the thinking didn’t last long: version 10.0 reimplements the Lock as the high bit in u2, overlaying the Blink and ShareCount.