Geoff Chappell, Software Analyst
The name KPRCB stands for (Kernel) Processor Control Block. The kernel keeps one for each logical processor, embedded in the processor’s KPCR. The KPRCB holds most of what the kernel needs ready access to while managing a processor and while managing resources that are themselves managed more simply (and quickly) per processor.
Kernel-mode code can easily find the KPRCB for whichever processor it’s executing on, by finding the current KPCR first. The latter is well-known to be accessible through the fs and gs registers in 32-bit and 64-bit Windows respectively. Its Prcb or CurrentPrcb member, again in 32-bit and 64-bit Windows respectively, points to the KPRCB without depending on it to be embedded in the KPCR.
Finding the KPRCB for an arbitrary processor is also easy on 64-bit Windows but is practically impossible on 32-bit Windows. The kernel does it by keeping an array of pointers to the KPRCB structures for all processors, but the address of this array is of course not exported. (That it is named KiProcessorBlock can be useful to know when debugging with symbols.) The 64-bit kernel exports a function, named KeQueryPrcbAddress, that looks up the array to locate the KPRCB for a given processor, but the closest that the 32-bit kernel lets code outside the kernel get to an arbitrary processor’s KPRCB is that a few functions copy from that KPRCB.
Presumably, any access that kernel-mode code running on one processor does obtain to a KPRCB for another processor should be used very carefully. Some may be safe to use while holding a lock. Some seems likely to be intended as read-only to other processors: much of the point to having per-processor data that can change while Windows runs is that if the data is updated only by code running on that processor then the updating can be done both quickly and safely without concern for synchronisation. As an aside, I suspect that more than a few things go very slightly wrong in kernel-mode Windows because of insufficient care when accessing per-processor data from a different processor.
Neither the KPRCB nor its containing KPCR is formally documented, but the existence of both is disclosed in header files from as far back as the Device Driver Kit (DDK) for Windows NT 3.51. Even in that DDK, the NTDDK.H file provides C-language definitions for what comments say is an “architecturally defined section of the PRCB” for the Alpha processor. The Windows 2000 DDK did the same for the IA64. Both were gone by the time of the Windows Driver Kit (WDK) for Windows Vista. The KPRCB for the i386 and amd64 was always left as opaque. For these, NTDDK.H presented an “architecturally defined section” of the KPCR, showing a a pointer to the KPRCB, but giving no definition.
For much of this time, however, the practical equivalent of C-language definitions in headers has been readily available as type definitions in symbol files ever since Windows 2003 SP3. Symbol files for the kernel have type definitions for the whole KPRCB. Starting with Windows XP, these turn up in other symbol files too, notably for NTDLL.DLL—yes, from user-mode, with no access to the KPRCB—and ACPI.SYS. Intriguingly, though the HAL is the primary external user of the KPRCB, type definitions do not appear in symbol files for HALs until 64-bit Windows Vista and 32-bit Windows 7, and they are anyway for a reduced KPRCB. The reductions turn up in other symbol files and are surely for that part of the KPRCB that counts as architecturally defined.
In a recent development, a header file, named NTOSP.H, in the Windows Driver Kit (WDK) for Windows 10 supplies C-language definitions of the KPRCB for the i386, amd64 and ARM processors (and also provides an inline function, named KeGetCurrentPrcb, that does the segment-based lookup described above). This appears to be Microsoft’s first formal disclosure in public of the structure’s layout for these processors. It is, of course, just of the architecturally defined section. Even for this, its value is lessened by having no conditional compilation blocks for accommodating earlier versions—and, worse, by a comment that states the section “will not change from version to version of NT” despite giving a layout that is immediately applicable only to programming that targets Windows 10 specifically. Since the same comment about not changing is in the NTDDK.H from the Windows NT 3.51 DDK, it is plausibly just an age-old statement of intention that never was honoured but also has never got cleaned up.
Being shared between the kernel and HAL, the “architecturally defined section” of the KPRCB, and even in some ways the whole structure, is more stable than it might be if internal to one module. Yet it plainly has been intended all along as the highly variable portion of what the kernel and HAL from the same build of Windows share about each processor. Some of the variability can be sensed just from the changing size:
Version | Size (x86) | Size (x64) |
---|---|---|
3.51 | 0x0360 | |
4.0 | 0x0558 | |
5.0 | 0x09F0 | |
5.1 | 0x0C50 | |
early 5.2 (before Windows Server 2003 SP1) | 0x0DD0 | |
late 5.2 (Windows Server 2003 SP1 and higher) | 0x0EC0 | 0x2480 |
early 6.0 (before windows Vista SP1) | 0x1F98 | 0x3A20 |
late 6.0 (Windows Vista SP1 and higher) | 0x2008 | 0x3B20 |
6.1 | 0x3628 | 0x4D00 |
6.2 | 0x4160 | 0x5B80 |
6.3 | 0x4508 | 0x5BC0 |
10.0 | 0x4900 | 0x6900 |
See that the size changes not just from version to version but even within versions. What is not clear just from the size is that the change from one version to another is not just by growth. Members have frequently been inserted, deleted and moved around—even from one end of the structure to the other and even after being moved into the “architecturally defined section” that’s supposed not to “change from version to version”.
The KPRCB differs enough between 32-bit and 64-bit Windows that detailed layouts are better presented separately (or will be, if ever a presentable presentation is devised):
For both processors, the KPRCB has members named MajorVersion and MinorVersion. They are at the same offsets—different for each processor, of course—in all known versions. In such a highly variable structure, the stability of two members that might be consulted to check the version is perhaps commendable but it is also entirely pointless: in all versions, these two members are reliably both 1.