Geoff Chappell - Software Analyst
The KDPC is the structure in which the kernel keeps the state of a Deferred Procedure Call (DPC). The latter is a routine that kernel-mode code can register with the kernel to be called back at DISPATCH_LEVEL. Since DISPATCH_LEVEL is not a friendly Interrupt Request Level (IRQL), the usual reason for scheduling a routine to execute at DISPATCH_LEVEL is that the IRQL at the time is even more restrictive, as when servicing a hardware interrupt.
In version 5.2 and higher, a KDPC can represent either a normal DPC, as described above, or a Threaded DPC. In the latter variant, if the kernel can arrange it, the scheduled procedure is called back at PASSIVE_LEVEL from a highest-priority thread. However, support can be disabled (or may have failed), and so a threaded DPC can be called at DISPATCH_LEVEL much as if it had been a normal DPC all along. Let the implication for programming be stressed: a threaded DPC’s execution at PASSIVE_LEVEL is a bonus that must not be assumed; a threaded DPC must be written such that it can execute safely at DISPATCH_LEVEL.
Deferred Procedure Calls have been documented from the beginning. Threaded DPCs are documented as being “available in Windows Vista and later versions.” Why they are not documented for Windows Server 2003 may be a mystery even at Microsoft. After all, the NTIFS.H from the Windows Driver Kit (WDK) for Windows Vista wraps its declaration of the KeInitializeThreadedDpc function in a conditional block for Windows Server 2003 and higher.
Though DPCs have always been documented, the content of the KDPC that supports the functionality has always been explicitly not documented. The KDPC is said to be “an opaque structure” and programmers are warned “do not set members of this structure directly.” Explicit warnings are perhaps necessary because a C-language definition has been provided in every Device Driver Kit (DDK) or WDK all the way back to Windows NT 3.1. The layout seems to have been published only so that where drivers and other kernel-mode modules create a KDPC they can know how much space to allocate. Since what happens in the space is entirely in the hands of kernel functions that are provided for initialising and then working with the object, Microsoft might as well have defined the KDPC as containing an array of bytes, with no consequences for programmers at large except if the size ever changed.
In all versions, the KDPC is 0x20 and 0x40 bytes in 32-bit and 64-bit Windows respectively. Constancy of size is not strictly required by the expectation of opacity in user-supplied memory but is very nearly so. The same opacity, however, means that interpretation within the constant size is free to change completely even between builds. The following shorthands apply throughout this article:
One complication to the description is that Windows 8.1 overlays the first four bytes with a 32-bit integer for simultaneous access.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x00 | 0x00 |
/* individual members, see below */ |
3.10 to 6.2 |
union { ULONG TargetInfoAsUlong; struct { /* individual members, see below */ }; }; |
6.3 and higher |
As the integer’s name suggests, these first four bytes mostly record the desired circumstances for executing the DPC. With the overlay aside, these first four bytes are:
Offset (x86) | Offset (x64) | Definition | Versions | Remarks |
---|---|---|---|---|
0x00 |
SHORT Type; |
3.10 to early 5.2 | ||
0x00 |
UCHAR Type; |
late 5.2 and higher | ||
0x01 | 0x01 |
UCHAR Importance; |
late 5.2 and higher | previously at 0x03 |
0x02 | 0x02 |
SHORT Size; |
3.10 to 3.50 | |
UCHAR Number; |
3.51 to 5.2 | |||
USHORT Number; |
early 6.0 only | |||
USHORT volatile Number; |
late 6.0 and higher | |||
0x03 |
UCHAR Importance; |
3.51 to early 5.2 | next at 0x01 | |
0x03 |
UCHAR Expedite; |
late 5.2 only |
As for other kernel objects, the Type at the start of a KDPC comes from the KOBJECTS enumeration. For the KDPC, the Type is specifically DpcObject for normal DPCs or, in version 5.2 and higher, ThreadedDpcObject for a threaded DPC. These values are set by the KeInitializeDpc and KeInitializeThreadedDpc functions, respectively, and then the Type is left alone. Note that the numerical values of DpcObject and ThreadedDpcObject are version-dependent:
It was not until version 3.51 that DPCs could either be prioritised or be targeted to a specific processor (represented by Number). The Importance takes its values from the KDPC_IMPORTANCE enumeration. It is MediumImportance (1) initially, but can be changed by calling the KeSetImportanceDpc function. When a later call to KeInsertQueueDpc inserts the KDPC into a list for deferred execution, it goes to the head of the list if Importance is HighImportance (2), else to the tail. For normal DPCs, the Importance also affects whether DPC processing is requested at the time of insertion.
Also new for version 3.51 was that a DPC could be targeted to a specific processor. The target processor is kept in the KDPC as the Number. It is set by calling the KeSetTargetProcessor function or, starting with Windows 7, KeSetTargetProcessEx. A later call to KeInsertQueueDpc inserts the KDPC into the corresponding per-processor list.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x04 | 0x08 |
LIST_ENTRY DpcListEntry; |
3.10 to 6.2 |
SINGLE_LIST_ENTRY DpcListEntry; |
6.3 and higher | ||
0x08 | 0x10 |
KAFFINITY ProcessorHistory; |
6.3 and higher |
The KeInsertQueueDpc function schedules a DPC by inserting the KDPC into a double-linked or single-linked list, depending on the version. At first, with no targeting of the DPC’s eventual execution to a selected processor, there was only one list for all DPCs. Version 3.50 introduced one list per processor, as the DpcListHead member of the KPRCB. In version 5.2 and higher, each processor has two lists, one for normal DPCs and one for threaded DPCs. Whichever list a KDPC is inserted into, it is linked into the list through the DpcListEntry member.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0C | 0x18 |
VOID (*DeferredRoutine) ( KDPC *, PVOID, PVOID, PVOID); |
all |
0x10 | 0x20 |
PVOID DeferredContext; |
all |
0x14 | 0x28 |
PVOID SystemArgument1; |
all |
0x18 | 0x30 |
PVOID SystemArgument2; |
all |
0x1C |
BOOLEAN Inserted; |
3.10 only | |
ULONG *Lock; |
3.50 to 5.1 | ||
0x38 |
PVOID DpcData; |
5.2 and higher |
The DeferredRoutine is, of course, the address of the routine that is to be called back. It is specified when the KDPC is initialised, and is thereafter left alone (unless the KDPC is re-initialised). It receives four arguments: the address of the KDPC; plus others that are retrieved from the KDPC. Of these, the DeferredContext is set with the DeferredRoutine when initialising the KDPC, but SystemArgument1 and SystemArgument2 are set afresh whenever the KDPC is inserted.
It is mere supposition that Inserted is Microsoft’s name for the BOOLEAN with which version 3.10 records that the KDPC is inserted in that version’s global list of all queued DPCs.
The C-language definitions in Microsoft’s headers have Lock pointing to a ULONG originally but to a ULONG_PTR starting with the DDK for Windows XP. This is appropriate, since what’s pointed to is specifically a spin lock, but as far as concerns x86 and x64 builds, at least while no x64 build of version 5.1 is known, the difference between ULONG_PTR and ULONG has no practical consequence. For all versions in question, an inserted KDPC has its Lock pointed to its target processor’s DpcLock in the KPRCB. (In version 3.50, the target processor is necessarily the current processor at the time of insertion.)
Though the DpcData member of the KDPC is declared as pointing to void in version 5.2 and higher, what it actually points to is a KDPC_DATA structure.