Geoff Chappell - Software Analyst
The !pfn command takes one documented parameter and dumps the corresponding MMPFN structure. Microsoft documents that the command can dump the entire page frame database but does not document how: !pfn 0 1 does it in all versions since 3.51. Some other undocumented incantations may be useful. Some are defective.
It will surprise nobody that Microsoft’s seemingly substantial Debugger Reference (still helpfully included with the debugger for offline use) omits very much that Microsoft’s debuggers can do. What may surprise is how old an undocumented feature can be yet remain undocumented. The undocumented functionality that this note presents of the !pfn command dates from the KDEXTX86.DLL debugger extension as supplied with the Device Driver Kit (DDK) for Windows NT 3.51.
Can this really have gone unnoticed for so long? Much of the point to debugger extension commands is that they’re easy to use. They are just as easily used differently from what’s documented. Where a command is documented as taking one argument, as is !pfn, it must sometimes happen that programmers enter a second argument by accident if not just for fun or mischief. If the command then behaves differently, and especially if it produces something useful, news of it soon circulates as folklore. Surely?
For the !pfn command in particular, Microsoft itself gives a big hint that there’s something to look for. It’s even fair to say that Microsoft documents that the command can do something undocumented. Starting with the DDK for Windows XP, the !pfn documentation begins with the following one-line summary of what the command can do:
The !pfn extension displays information about a specific page frame or the entire page frame database.
Presumably, this sentence was not composed without someone at Microsoft having thought that dumping the entire page frame database may be useful (and without their being aware that this extra functionality is undocumented). But then what’s given as the syntax is only
!pfn PageFrame
and the one argument is described as
Specifies the hexadecimal number of the page frame to be displayed.
Where then is the means of displaying information about the “entire page frame database”? The answer, of course, is that what Microsoft documents as the syntax is incomplete.
!pfn PageFrame [Flags [Partition]]
The PageFrame parameter is ordinarily a page frame number. It can instead be the address of an MMPFN structure if the Flags parameter is zero or is missing.
The Flags, for want of a better name, select from different methods of progressing through the database.
The Partition is the address of a memory partition object. This parameter is ignored unless Flags is non-zero.
The MASM expression evaluator is forced for the first two parameters. The current expression evaluator is used for the third. For all, the expression can contain aliases.
The !pfn command is ancient. It’s among the few dozen that are built into the I386KD.EXE from the DDK for Windows NT 3.1. It was surely transferred to the first debugger extensions. Certainly it’s implemented as a debugger extension command by KDEXTX86.DLL in the DDK for Windows NT 3.51. It was then transferred to KDEXTS.DLL with the introduction of the Debugger Engine (DBGENG.DLL) in Windows XP. I do not intend to track the changes of behaviour through all these versions. This note is prepared from inspection of KDEXTS version 10.0.16299.91, which I refer to below as this note’s reference version.
At the heart of the !pfn command is that it dumps the MMPFN structure. The command’s modern implementation requires at the very least that the debugger has the use of a symbol file for the kernel and that this symbol file has type information for nt!_MMPFN. This type information is present in all known public symbol files for the kernel starting from Windows 2000 SP3.
Less obvious is that although Microsoft documents the !pfn command as supported through KDEXTS in “Windows XP and later”, the !pfn command depends on memory-manager state that this note’s reference version of KDEXTS does not initialise unless the Windows build number at the target machine is at least 7600, i.e., unless the target machine is running Windows 7 or higher. Try to use the !pfn command when debugging an earlier version and you will be told
Target machine operating system not supported
As suggested by Microsoft’s one-line description, the !pfn command has two modes. Given one parameter, or zero as the second, the !pfn command dumps one MMPFN structure. Otherwise, the !pfn command dumps potentially very many MMPFN structures, including the whole of them.
For dumping one MMPFN structure, the !pfn command’s value relative to using the dt command to inspect the type directly is:
For any given page frame number, the corresponding MMPFN can be located by using the page frame number as an index into an array whose address is given by the internal variable MmPfnDatabase. The debugger may instead know this address from the same-named KDDEBUGGER_DATA member. Before the 1607 release of Windows 10, programmers also have the shortcut of knowing that 64-bit Windows keeps its MMPFN array at the preset address 0xFFFFFA80`00000000.
If the PageFrame parameter is less than what the debugger knows for MmPfnDatabase, then it is a page frame number. Otherwise, since it is at or above the beginning of the MMPFN array, it is interpreted as an address in the array and the command dumps the MMPFN that begins at or contains this address. Either way, the command does not validate that the address it computes for the MMPFN is not beyond the MmPfnDatabase array (or has not wrapped round).
The dump continues for multiple lines, necessarily including the following five:
PFN pfn at address mmpfn flink flink blink / share count blink pteaddress pteaddr reference count ref used entry count used cache color color Priority priority restore pte original containing page pteframe location flagcodes flagtext
The first line tells which MMPFN is being dumped, both by the page frame number of the physical page that the MMPFN describes and by the address of the MMPFN itself. The remaining lines are resolved from members of the MMPFN:
Placeholder | MMPFN Source | Remarks |
---|---|---|
flink | u1.Flink | |
blink | u2.Blink | |
pteaddr | PteAddress | |
ref | u3.e2.ReferenceCount | |
used | UsedPageTableEntries OriginalPte.u.Soft.UsedPageTableEntries |
x64 only |
cache | u3.e1.CacheAttribute | plain-text representation, see below |
color | u4.PageColor u3.e1.PageColor |
|
priority | u3.e3.Priority u3.e1.Priority |
|
original | OriginalPte | |
pteframe | u4.PteFrame PteFrame |
|
location | u3.e1.PageLocation | plain-text representation, see below |
flagcodes | various | single-character codes, see below |
flagtext | various | plain-text representations, see below |
See that the command resolves some fields from alternative locations in the MMPFN structure. This is a heavily overloaded structure which Microsoft has evidently been determined should not grow. Over the nearly 20 years of Windows versions that the !pfn command is documented as supporting, many of the MMPFN members have moved around, even in terms of the symbols that are needed for referring to them programmatically. For all the convenience of this command’s sparing the programmer from the details, there is the occasional oversight (whether of the coding or of getting “Windows XP and later” updated in the documentation): for instance, before version 6.0, the priority would need to be found from u4.Priority.
The 2-bit CacheAttribute takes its values from the MI_PFN_CACHE_ATTRIBUTE enumeration. What the !pfn command resolves these to for the cache placeholder is:
The 3-bit PageLocation takes its values from the MMLISTS enumeration. What the !pfn command resolves these to for the location placeholder is:
The location is specially important for interpreting what shows for flink and blink. A location other than Active or Trans means that the MMPFN is in one or another list. The flink and blink then are page frame numbers of the next and previous MMPFN in the same list. For pages whose location is Active or Trans, the four or eight bytes that hold the u1.Flink and u2.Blink members are available for other use, hence their definitions as members of unions. The most notable other use is indicated by the output’s header: the blink may instead be a share count. However, the output does not get this share count from the u2.ShareCount member. What shows for flink and blink is only ever obtained from u1.Flink and u2.Blink. In recent versions, one (in 32-bit Windows) or both (in 64-bit) is only a bit field so that what shows for flink and blink is only a portion of what other use is made of the same space. This is problematic in 64-bit Windows since flink may show just the first 36 bits of a 64-bit pointer. To see the whole pointer, you must resort to some alternative such as dumping the MMPFN in detail by feeding its address to the dt command.
Eight single-bit MMPFN members are described in the flagcodes and flagtext placeholders, all but one in both:
Code | Text | Source |
---|---|---|
M | Modified | u3.e1.Modified |
P | Shared | u4.PrototypePte u3.e1.PrototypePte |
R | ReadInProgress | u3.e1.ReadInProgress |
W | WriteInProgress | u3.e1.WriteInProgress |
E | InPageError | u3.e3.InPageError u3.e1.InPageError u4.InPageError |
X | ParityError | u3.e3.ParityError u3.e1.ParityError |
Y | RemovalRequested | u3.e3.RemovalRequested u3.e1.RemovalRequested |
V | VerifierAllocation | u4.VerifierAllocation u3.e1.VerifierAllocation |
LockedDown | see below |
Additional lines of output are possible between these two representations of 1-bit flags. Both are outside the present scope of this note. Information can be shown about the page’s inclusion in a memory partition, as determined from the u4.Partition member. Even more information, in this case about large and even huge pages, according to whether PteFrame has any of several special values, will not in practice be seen outside Microsoft—at least not by those of us who have only the public symbol files—since it requires type information for an internal variable named nt!MiLargePageSizes. This also applies, by the way, to LockedDown in what is otherwise the plain-text representation of single-bit flags.
When given a non-zero Flags parameter, the !pfn command displays one line of information for each of potentially very many page frames—including the whole of them, just as promised by Microsoft’s description. Zero for the PageFrame parameter has a conventional meaning of surveying the MmPfnDatabase from start to end in the straightforward order. Anything other than zero for the PageFrame is a page frame number from which to start the survey, which is then filtered in various ways depending on the starting page and on the Flags.
This note presently simplifies by ignoring that the Partition parameter can constrain the survey just to pages that belong to the given partition.
For the straightforward survey of the whole database, give zero for the PageFrame. The Flags are immaterial, except for zero and seven. The command !pfn 0 0 just dumps the MMPFN for page frame 0 as if Flags had been omitted. Since this MMPFN is empty, the dump is not useful (except as another way to obtain the base address of the MMPFN array). The command !pfn 0 7 attempts to follow an OriginalPte chain (see below) from this empty MMPFN for page frame 0, which becomes infinite.
When dumping the entire page frame database, successive MMPFN structures in the array are each described by one line, all under the one header:
Page Flink Blk/Shr Ref V PTE Address SavedPTE Frame State pfn flink blink ref pteaddr address original pteframe location flagcodes
This one-line dump for each MMPFN necessarily misses some of what is shown over multiple lines when asking about just one MMPFN. Apart from fields that are omitted entirely, there is also that the flagcodes in this one-line dump do not allow for Y or V. There is, however, an addition, even a useful one: the address is the one virtual address that maps to the physical page, if there is indeed just one, else is zero if the page is shared.
Dumping the whole of the page frame database can of course take very many minutes. Memory is fetched from the target in blocks of up to 2000 MMPFN structures. The dump is easily cancelled because the command checks for termination, e.g., through Ctrl-Break, after interpreting each structure.
With a non-zero PageFrame and non-zero Flags, the survey is only of some of the database:
The selected MMPFN structures are each described by one line, all under the one header, as above except to append the priority (in decimal):
Page Flink Blk/Shr Ref V PTE Address SavedPTE Frame State pfn flink blink ref pteaddr mappedva original pteframe location flagcodes priority
If the starting page’s location is Active or Trans, the dump necessarily continues in increasing order of page frame number: Flags is ignored.
For any other location, the starting page is in a list and the dump can usefully follow the list’s links, either forwards or backwards. That the !pfn command can do this obviously very helpful work goes back at least to the implementation in the KDEXTX86.DLL from the DDK for Windows NT 3.51. In those simple days, the list is ordinarily followed forwards, with u1.Flink as the page frame number to proceed to, but backwards, using u2.Blink, if Flags is 3.
Nowadays, strong caution is in order. More values are recognised for the Flags, but some are coded defectively. The following summary is only of what seems to be intended:
Flags | Description | Requirements |
---|---|---|
1 (or any non-zero not below) | forward through list | |
2 | forward through list but for the same NUMA node | starting page has Standby as location;
target is 64-bit Windows build 8000 or higher |
3 | backward through list | |
4 | backward through list but for the same NUMA node | starting page has Standby as location;
target is 64-bit Windows build 8000 or higher |
5 | forward through OriginalPte chain | |
7 | backward through OriginalPte chain | |
9 | up through PteFrame chain |
For cases 2 and 4 for Flags, failure to meet the Requirements produces the complaints:
PFN pfn is not a standby page
or
This build does not support per-NUMA-node standby lists
Most defects seem to originate with the introduction of per-NUMA-node standby lists in advance of 64-bit Windows 8. These have their own forward and backward links, each split into high and low parts according to where they could be made to fit. What KDEXTS does to piece these links together has little in common with how the kernel (now) does it. The thinking here is that KDEXTS retains code that actually does work for some pre-release build of Windows 8 but which never got updated as the kernel’s implementation evolved. This might explain why KDEXTS depends on the MMPFN to have a member named StandbyBits which is shown in no public symbol file for any of the kernel versions that are known to this study. However this mismatch of interpretation came about, the practical result is that cases 1, 2, 3 and 4 for Flags are defective if the starting page’s location is Standby and the target is 64-bit Windows build 8000 or higher.
See that for standby pages, the defect applies to the ordinary forward and backward links, not just the per-NUMA-node links. KDEXTS interprets the Flink and Blink very differently for pages on standby lists than for others. No evidence exists of interpretation through formally defined bit fields for the per-node links. KDEXTS instead makes its own interpretation of the Flink and Blink in two equal parts and picks up spare bits from a miscellany of other locations:
Link in Standby List | KDEXTS Interpretation | Kernel Interpretation |
---|---|---|
ordinary forward link | high 32 bits from low 32 bits of Flink;
low 4 bits from StandbyBits |
all 36 bits of Flink |
ordinary backward link | high 32 bits from low 32 bits of Blink;
low 4 bits from VaType |
all 36 bits of Blink |
per-NUMA-node forward link | high 32 bits from high 32 bits of Flink;
low 4 bits from low 4 bits of ViewCount |
high 28 bits from NodeFlinkHigh; low 8 bits from NodeFlinkLow |
per-NUMA-node backward link | high 32 bits from high 32 bits of Blink;
low 4 bits from high 4 bits of ViewCount |
high 20 bits from NodeBlinkHigh; low 16 bits from NodeBlinkLow |
This is as good a reminder as any that internal detail surmised from a debugging aid is not certainly correct. Debugger extensions, in particular, are written separately from whatever component they have internal knowledge of. Such knowledge can be out of date, as looks to have happened here, or can be wrong because the debugger extension and the component are built differently. Examples are even known of incorrect type information in public symbol files, as with the PROCESSINFO and THREADINFO structures for Windows 7.
In the OriginalPte chain for cases 5 and 7 of the Flags, KDEXTS interprets the OriginalPte.u.Long in one MMPFN as the page frame number of the next in the chain. This is not, of course, what the OriginalPte ordinarily holds. The circumstances in which the whole of it is a page frame number are not presently understood for recent Windows versions.
Even if an MMPFN does have an OriginalPte chain, following the chain backwards has no formal support. What KDEXTS does when Flags is 7 is to search the whole database from the start for the first MMPFN whose OriginalPte.u.Long is the page frame number to move backwards from. Presumably in anticipation of repeating this search and eventually of having to search to the end, giving 7 for Flags causes KDEXTS to fetch the whole of the page frame database from the target before even looking at the MMPFN for PageFrame. It fetches the database in blocks of up to 2000 MMPFN structures, just as when dumping the whole database, but instead of showing a header and dumping a line for each MMPFN, it instead shows one period for every 256 page frames, presumably to assure that progress is being made.
Unfortunately, the OriginalPte.u.Long in an MMPFN can be within range for a page frame number without being intended as one: if the !pfn command with 5 or 7 for Flags doesn’t stop with the start page, it typically hangs (needing termination by Ctrl-Break).
See that the !pfn dump of a single MMPFN presents the u4.PteFrame (previously a direct PteFrame member) as a containing page. It’s only natural to ask for the containing page of the containing page and thence to make a PteFrame chain. Some hint of the usefulness is that Microsoft’s own example in the !pfn documentation shows two !pfn commands, one for the containing page of the other. Showing the PteFrame chain seems to be the intention when Flags is 9. The usefulness, however, is greatly diminished because successive pages in this chain will not have the same location and therefore don’t show in the dump. This does not matter in practice, though, because the dump is anyway defective. The chain must terminate at the highest level of the page mapping algorithm, i.e., with a page that is its own containing page, but KDEXTS does not allow for this and so the !pfn dump with 9 for Flags hangs.