KPCR (amd64)

The name KPCR stands for (Kernel) Processor Control Region. The kernel keeps a KPCR (formally a _KPCR) for each logical processor. The KPCR is highly specific to the processor architecture. This page concerns itself only with the KPCR in 64-bit Windows for the processor architecture that’s variously named amd64 or x64. The x86 KPCR is presented separately.

Access

Kernel-mode code can easily find the KPCR for whichever processor it’s executing on, because when the processor last entered ring 0, however it got there, the kernel will have loaded the gs register to address that processor’s KPCR. This is done by the swapgs instruction. It loads the base address for gs from the processor’s Model Specific Register (MSR) 0xC0000102, which the kernel initialises with the address of the processor’s KPCR. Intel’s label for this MSR is IA32_KERNEL_GS_BASE. Microsoft’s assembly-language name, defined in KSAMD64.INC, is MSR_GS_SWAP.

The KPCR conveniently holds its own address in the Self member so that reading just this one member using a segment register override makes the whole KPCR accessible without overrides. This is so fundamental that all versions of the 64-bit Windows kernel have it as an internal routine, typically inlined, which is coded very much like:

FORCEINLINE
KPCR *KeGetPcr (VOID)
{
    return (KPCR *) __readgsqword (FIELD_OFFSET (KPCR, Self));
}

Beware, though, that this is the address of the KPCR for the processor that the thread was running on at the time. It remains the address of the current KPCR only while the thread can ensure it is not switched to another processor. I suspect that more than a few things go very slightly wrong in kernel-mode Windows because this point is insufficiently respected.

Documentation Status

Not even the role of the segment registers in accessing the KPCR is formally documented, but the KPCR is made at least semi-official by C-language definitions that Microsoft has published in every applicable Device Driver Kit (DDK) or Windows Driver Kit (WDK).

That said, it must be pointed out that the best-known of such C-language definitions, in NTDDK.H, is incomplete.

Variability

Whatever was or is the intention, e.g., that the KPCR before the KPRCB is per-processor information that the kernel shares but the KPRCB itself is (more) private to the kernel, one practical consequence is that the start of the KPCR is highly stable across Windows versions while the KPRCB is highly changeable.

Indeed, except for changes within the embedded KPRCB, the x64 KPCR is so stable that although the structure provides for MajorVersion and MinorVersion numbers, they have never changed—they are both 1 in all versions—and arguably have never needed changing. Except for the early reuse of one member of the unnamed structure that overlays the NT_TIB at the beginning, the x64 KPCR is completely stable. The changing size of the whole KPCR structure, as shown in the table below, is due entirely to the changing size of the KPRCB at the structure’s end. All versions have this embedded KPCR at offset 0x0180. It can be convenient, if not formally correct, to think of the KPCR as just these first 0x0180 bytes.

Version Size
late 5.2 0x2600
early 6.0 (before SP1) 0x3BA0
late 6.0 0x3CA0
6.1 0x4E80
6.2 0x5D00
6.3 0x5D40
10.0 to 1607 0x6A80
1703 0x68C0
1709 0x6B00
1803 to 1809 0x8040
1903 0x9080
2004 0xB080

Layout

In the tables that follow, C-language definitions are reconstructed from type information in symbol files that Microsoft publishes for the kernel and from definitions in the NTDDK.H files from development kits for driver programming.

Offset Definition
0x00
union {
    NT_TIB NtTib;
    struct {
        /*  slightly changing members, see below  */
    };
};
0x38
KIDTENTRY64 *IdtBase;
0x40
ULONG64 Unused [2];
0x50
KIRQL Irql;
0x51
UCHAR SecondLevelCacheAssociativity;
0x52
UCHAR ObsoleteNumber;
0x53
UCHAR Fill0;
0x54
ULONG Unused0 [3];
0x60
USHORT MajorVersion;
0x62
USHORT MinorVersion;
0x64
ULONG StallScaleFactor;
0x68
PVOID Unused1 [3];
0x80
ULONG KernelReserved [0x0F];
0xBC
ULONG SecondLevelCacheSize;
0xC0
ULONG HalReserved [0x10];
0x0100
ULONG Unused2;
0x0108
PVOID KdVersionBlock;
0x0110
PVOID Unused3;
0x0118
ULONG PcrAlign1 [0x18];
0x0180
KPRCB Prcb;

It seems at least plausible that the unused space ahead of KernelReserved exists to place the latter and thus also HalReserved at 64-byte cache-line boundaries. The different sizing of KernelReserved and HalReserved seems to have been inherited from the 32-bit implementation’s creation of SecondLevelCacheSize at the end of a previously larger KernelReserved.

Curiously, PcrAlign1 does not extend exactly to the Prcb that follows. That Prcb is meant to be cache-aligned is certain. Cache alignment is plainly a recurring concern within the KPRCB and is obviously simpler to arrange if the KPRCB is itself cache aligned. That it isn’t for 32-bit Windows is a recurring trap for Microsoft’s programmers. That it is aligned for 64-bit Windows is evidently nothing to do with PcrAlign1.

NT_TIB Overlay

The NT_TIB at the beginning of the KPCR is given in union with an unnamed structure whose members change a little between versions:

Offset Definition Versions Remarks
0x00
KGDTENTRY64 *GdtBase;
   
0x08
KTSS64 *TssBase;
   
0x10
PVOID PerfGlobalGroupMask;
late 5.2 only  
ULONG64 UserRsp;
6.0 and higher previously at 0x20 in KPRCB
0x18
KPCR *Self;
   
0x20
KPRCB *CurrentPrcb;
   
0x28
KSPIN_LOCK_QUEUE *LockArray;
   
0x30
PVOID Used_Self;
   

This unnamed structure of course models the actual use. In effect, the KPCR begins with a kernel-mode NT_TIB that is nothing like the user-mode NT_TIB that is defined in NTDDK.H and WINNT.H. Both have the same size, both end with something that’s presented as some sort of pointer to itself, and there the similarities end.

In version 5.2, what would be the StackLimit in a user-mode NT_TIB instead holds the address of a PERFINFO_GROUPMASK. This is an array of bits for what types of event are enabled in NT Kernel Logger sessions. The intention was presumably to allow the tracing of different selections of events on different processors. Whatever the plan, it didn’t survive even to version 6.0.

In a user-mode NT_TIB, the last member is named Self, is formally a pointer to an NT_TIB, and does indeed point to the start of the NT_TIB. In a kernel-mode NT_TIB, it is named Used_Self and is formally a pointer to void. If it points to anything, it is to an NT_TIB but to a different NT_TIB—not the one at the start of the KPCR but to the user-mode NT_TIB at the start of the TEB for whichever thread is currently running on the processor.