KDPC

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.

Documentation Status

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.

Layout

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.