Geoff Chappell, Software Analyst
The name KPCR stands for (Kernel) Processor Control Region. The kernel keeps a KPCR for each logical processor. The KPCR for the boot processor is in space provided by the loader or is in the kernel’s .data section, but the KPCR for each additional processor is at the start of a large-scale per-processor state that the kernel builds in one memory allocation. In version 6.0, for instance:
The precise arrangement of these items is plainly meant to be the kernel’s own business. Explicit use of them, wherever they are for any processor, doesn’t look to be intended for any software other than the kernel and the HAL. They are listed here just for the general background of knowing what sorts of large-scale structures Windows keeps for each processor and to elaborate the architectural point that the KPCR is the means through which the kernel manages them.
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 fs or gs register, in 32-bit and 64-bit Windows respectively, to address that processor’s KPCR. In 32-bit Windows, entering kernel mode gets fs loaded with a selector (0x0030) for a segment whose base address, as determined from the processor’s GDT which the kernel initialised long before, is that of the processor’s KPCR. Entering kernel mode in 64-bit Windows gets the base address for gs loaded via the swapgs instruction from the processor’s Machine Specific Register (MSR) 0xC0000102 which the kernel initialises with the address of the processor’s KPCR.
The KPCR conveniently holds its own address in the SelfPcr or Self member, in 32-bit and 64-bit Windows respectively, so that reading just this one member using a segment register override makes the whole KPCR accessible without overrides. 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. As an aside, I suspect that more than a few things go very slightly wrong in kernel-mode Windows because this point is insufficiently respected.
Before version 6.0, space for the initial KPCR, for the boot processor, is allocated by the loader at the fixed address 0xFFDFF000, perhaps so that the kernel can have the memory available for initialising its use of the boot processor before initialising its own memory manager but without needing to carry the relatively large space in its file image. The NTDDK.H from the Device Driver Kit (DDK) for Windows XP defines this address as KIP0PCRADDRESS. If the loader ends up running the 64-bit kernel, the fixed address is unmapped and the 64-bit kernel instead has space in its .data section for the initial KPCR. The 32-bit kernel, however, keeps the initial KPCR at the fixed address it gets from the loader—and the single-processor builds, for which the initial KPCR is their one and only, use the fixed address in preference to the fs register.
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 definitions in header files from the DDK or Windows Driver Kit (WDK). From all the way back to the DDK for Windows NT3.51, the NTDDK.H file defines a KPCR for each of the supported architectures and presents either inline functions or macros which access the KPCR via the fs or gs register using compiler intrinsics or inline assembly language.
A comment speaks of an “architecturally defined section of the PCR” which “may be directly addressed by vendor/platform specific HAL code and will not change from version to version of NT”. A ready inference is that this defined section is just part of an undisclosed whole. Type definitions in symbol files for the kernel confirm that the definition in NTDDK.H is not what Microsoft itself uses when building the kernel. Definitions in driver kits from before Windows Vista stop at Number (which is as far as needed for the inline function KeGetCurrentProcessorNumber). All stop short of the embedded KPRCB.
The comment is anyway best taken as stating some intention which is not strictly observed in practice. Certainly, the “architecturally defined section” is not all that the HAL has access to. It may be all that the HAL accesses from code written in C—and, indeed, the symbol files for the HAL have the NTDDK.H definition of the KPCR—but parts of the HAL that are written in assembly language access KPRCB members very nearly at the end (e.g., HighCycleTime) by knowing their offsets from the start of the KPCR. The ACPI.SYS driver also transgresses the comment: it reads the CurrentThread member from the KPRCB by knowing what offset to use with the segment override.
Still, whatever was or is the intention, e.g., that the KPCR before the KPRCB is per-processor information that the kernel shares while 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, the KPCR is so stable that although the structure provides for MajorVersion and MinorVersion numbers, they have not needed changing: they are both 1 in all known builds of both 32-bit and 64-bit Windows. Though a few members have gone in or out of use, including one that was first unused and then redefined for reuse, no members that aren’t labelled reserved or spare have ever shifted.
In the tables that follow, C-language definitions are reconstructed from type definitions in symbol files that Microsoft publishes for the kernel and from definitions in the NTDDK.H files from development kits for driver programming. The KPCR varies enough between 32-bit and 64-bit Windows that the layouts are better presented separately.
The symbol-file type definitions are first available for Windows 2003 SP3. For earlier versions, members after Number are therefore not known with certainty: some notes on this point follow the table.
Disregard the embedded KPRCB, and the KPCR is 0x0120 bytes in all known builds of 32-bit Windows.
Offset | Definition | Versions |
---|---|---|
0x00 |
NT_TIB NtTib; |
3.51 to 5.1 |
union { NT_TIB NtTib; struct { /* slightly changing members, see below */ }; }; |
5.2 and higher | |
0x1C |
KPCR *SelfPcr; |
|
0x20 |
KPRCB *Prcb; |
|
0x24 |
KIRQL Irql; |
|
0x28 |
ULONG IRR; |
|
0x2C |
ULONG IrrActive; |
|
0x30 |
ULONG IDR; |
|
0x34 |
ULONG Reserved2; |
3.51 to 5.0 |
PVOID KdVersionBlock; |
5.1 and higher | |
0x38 |
KIDTENTRY *IDT; |
|
0x3C |
KGDTENTRY *GDT; |
|
0x40 |
KTSS *TSS; |
|
0x44 |
USHORT MajorVersion; |
|
0x46 |
USHORT MinorVersion; |
|
0x48 |
KAFFINITY SetMember; |
|
0x4C |
ULONG StallScaleFactor; |
|
0x50 |
UCHAR DebugActive; |
3.51 to 5.1 |
UCHAR SpareUnused; |
5.2 and higher | |
0x51 |
UCHAR Number; |
|
0x52 |
UCHAR VdmAlert; |
3.51 to 5.0 |
UCHAR Spare0; |
5.1 and higher | |
0x53 |
UCHAR Reserved [1]; |
3.51 to 5.0 |
UCHAR SecondLevelCacheAssociativity; |
5.1 and higher | |
0x54 |
ULONG KernelReserved [0x10]; |
3.51 to 4.0 |
ULONG KernelReserved [0x0F]; |
5.0 only | |
ULONG VdmAlert; |
5.1 and higher | |
0x58 |
ULONG KernelReserved [0x0E]; |
5.1 and higher |
0x90 |
ULONG SecondLevelCacheSize; |
5.0 and higher |
0x94 |
ULONG HalReserved [0x10]; |
|
0xD4 |
ULONG InterruptMode; |
|
0xD8 |
UCHAR Spare1; |
|
0xDC |
ULONG KernelReserved2 [0x11]; |
|
0x0120 |
KPRCB PrcbData; |
At any given moment, the 8-bit Irql member is the processor’s current IRQL. It is what the long-documented HAL function KeGetCurrentIrql looks up. As suggested by the comment “do not use 3 bytes after this as HALs assume they are zero” from the NTDDK.H in the DDK for Windows Server 2003, the HAL sometimes sets 32 bits for this member, e.g., in KeTryToAcquireQueuedSpinLock up to and including version 6.1, and even as late as version 10.0 when restoring the Irql after handling a Machine Check exception (interrupt 0x12).
What KdVersionBlock actually points to is an internal kernel variable that is also named KdVersionBlock. This variable is a DBGKD_GET_VERSION64 structure, which is defined in the WDK header file WDBGEXTS.H. This structure’s reason for existence is presumably to provide the means for a kernel-mode debugger to know more detail about the kernel it’s working with. Among other things, the structure has pointers to numerous kernel variables that are otherwise internal to the kernel, i.e., are not exported. Exposing the KdVersionBlock variable via the KPCR means that all those otherwise internal variables are easily and reliably accessible to all kernel-mode software (including kernel-mode malware).
The SecondLevelCacheAssociativity and SecondLevelCacheSize are determined when initialising the kernel’s use of the processor, but only if the CPU vendor string is one of the following:
For CPUs from other vendors the size and associativity are zero. The kernel is not known to have any Second-Level (L2) Cache Support before version 5.0, which anyway does not bother about associativity. The layout above supposes as the most plausible history that the kernel and HAL reservations were originally the same size and that version 5.0 took SecondLevelCacheSize from the end of the previously larger KernelReserved.
The HAL certainly does use its HalReserved area, but the kernel knows nothing of what’s inside and type definitions in symbol files for the HAL seem not to cover it.
No use is known of the InterruptMode, in any version, or of Spare1 before it became spare (if it ever was in use). Whether they are defined for versions before 5.0 (or, strictly speaking, from before the build of version 5.0 for Windows 2000 SP3) is not known. Something like KernelReserved2 will have been defined in those versions, if only to set the PrcbData at the reliable offset of 0x0120.
Starting with Windows Server 2003, 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 |
---|---|---|
0x00 |
EXCEPTION_REGISTRATION_RECORD *Used_ExceptionList; |
5.2 and higher |
0x04 |
PVOID Used_StackBase; |
5.2 and higher |
0x08 |
PVOID PerfGlobalGroupMask; |
5.2 and higher |
PVOID Spare2; |
6.0 to 6.2 | |
ULONG MxCsr; |
6.3 and higher | |
0x0C |
PVOID TssCopy; |
5.2 and higher |
0x10 |
ULONG ContextSwitches; |
5.2 and higher |
0x14 |
KAFFINITY SetMemberCopy; |
5.2 and higher |
0x18 |
PVOID Used_Self; |
5.2 and higher |
Disregard the embedded KPRCB, and the KPCR is 0x0180 bytes in all known builds of 64-bit Windows. Disregard the reuse of one member of the unnamed structure that overlays the NT_TIB at the beginning, and the 64-bit KPCR is completely stable.
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. That Prcb is intentionally 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.
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 |
---|---|---|
0x00 |
KGDTENTRY64 *GdtBase; |
|
0x08 |
KTSS64 *TssBase; |
|
0x10 |
PVOID PerfGlobalGroupMask; |
late 5.2 only |
ULONG64 UserRsp; |
6.0 and higher | |
0x18 |
KPCR *Self; |
|
0x20 |
KPRCB *CurrentPrcb; |
|
0x28 |
KSPIN_LOCK_QUEUE *LockArray; |
|
0x30 |
PVOID Used_Self; |