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