KTHREAD (3.51 to 5.1)

The KTHREAD structure is the Kernel Core’s portion of the ETHREAD structure. The latter is the thread object as exposed through the Object Manager. The KTHREAD is the core of it.


The KTHREAD structure is plainly internal to the kernel and its layout changes greatly between Windows versions and even between builds. For a good run of the early Windows versions, however, the KTHREAD is very stable. The size hardly changes: 0x01B0 bytes until the first growth, to 0x01C0 bytes, for version 5.1. Within the structure, there is some reordering—for instance, KernelStack and Teb get swapped for version 4.0 (plausibly as a side-effect of inserting TlsArray)—but not enough to confound presentation. In the layout table below, a handful of members each make two appearances because of reordering. These duplications are indicated in the Remarks column.

The progression to version 5.2 rearranges the structure on another scale. It does happen, of course, that even large sequences are kept together. More common, however, are such examples as the single-byte members Iopl, NpxState, Saturation and Priority, which are consecutive in versions 3.51 to 5.1 but are scattered throughout the structure—to offsets 0x01B9, 0x2D, 0x010D, 0x5B, respectively—for the first build of version 5.2. Extending the layout to version 5.2 can only produce a hopeless jumble. Instead, for each member that survives to version 5.2 the Future column points the way to that member’s appearance in a separate table for the KTHREAD in early builds of version 5.2.


Offsets, types and names in the tables that follow, are from Microsoft’s symbol files for the kernel starting with Windows 2000 SP3. Since symbol files for earlier versions do not contain type information for the KTHREAD, Microsoft’s names and types are something of a guess from inspection of how the kernel in those versions uses the KTHREAD. Where use of a member corresponds closely with that of a version for which type information is available in Microsoft’s symbol files, it seems reasonable to suppose continuity. Some use, however, has no correspondence, the code having changed too much. Even where the use hasn’t changed so much, tracking down the correspondence exhaustively would be difficult, if not impossible, even with source code.

It is well known that the KTHREAD begins with a DISPATCHER_HEADER in which the Type is 6, i.e., ThreadObject in the KOBJECTS enumeration. In these early versions, only the Type and Size distinguish the Header from that of any other dispatcher object.

Offset (x86) Definition Versions Remarks Past and Future
3.51 to 5.1   previously at 0x00;
next at 0x00
LIST_ENTRY MutantListHead;
3.10 to 5.1   previously at 0x10;
next at 0x10
PVOID InitialStack;
3.51 to 5.1   previously at 0x0168;
next at 0x18
PVOID StackLimit;
3.51 to 5.1   next at 0x1C
0x20 (3.51)
PVOID KernelStack;
3.51 only next at 0x28 previously at 0x016C
0x24 (3.51);
3.51 to 5.1   previously at 0x0170;
next at 0x30
PVOID TlsArray;
4.0 to 5.1   next at 0x01A4
PVOID KernelStack;
4.0 to 5.1 previously at 0x20 next at 0x20
0x28 (3.51);
BOOLEAN DebugActive;
3.51 to 5.1   previously at 0x0199;
next at 0x03 in Header
0x29 (3.51);
UCHAR State;
3.51 to 5.1   previously at 0x01A2;
next at 0x2C
0x2A (3.51);
BOOLEAN Alerted [MaximumMode];
3.51 to 5.1   previously at 0x0195;
next at 0x5E
0x2C (3.51);
3.51 to 5.1   previously at 0x01AC;
next at 0x01B9
0x2D (3.51);
UCHAR NpxState;
3.51 to 5.1   previously at 0x01AD;
next at 0x2D
0x2E (3.51);
CHAR Saturation;
3.51 to 5.1   next at 0x010D
0x2F (3.51);
CHAR Priority;
3.51 to 5.1   previously at 0x01A1;
next at 0x5B
0x30 (3.51);
3.51 to 5.1   previously at 0x0130;
next at 0x34
0x48 (3.51);
ULONG ContextSwitches;
3.51 to 5.1   previously at 0x0174;
next at 0x28
UCHAR IdleSwapBlock;
5.1 only    
UCHAR Spare0 [3];
5.1 only    
0x4C (3.51);
0x50 (4.0 to 5.0);
LONG WaitStatus;
3.51 to 5.1   previously at 0x0190;
next at 0x50
0x50 (3.51);
0x54 (4.0 to 5.0);
KIRQL WaitIrql;
3.51 to 5.1   previously at 0x01A5;
next at 0x2E
0x51 (3.51);
0x55 (4.0 to 5.0);
3.51 to 5.1   previously at 0x01A6;
next at 0x2F
0x52 (3.51);
0x56 (4.0 to 5.0);
3.51 to 5.1   previously at 0x019D;
next at 0x59
0x53 (3.51);
0x57 (4.0 to 5.0);
UCHAR WaitReason;
3.51 to 5.1   previously at 0x01A7;
next at 0x5A
0x54 (3.51);
0x58 (4.0 to 5.0);
KWAIT_BLOCK *WaitBlockList;
3.51 to 5.1   previously at 0x018C;
next at 0x54
0x58 (3.51);
0x5C (4.0 to 5.0);
LIST_ENTRY WaitListEntry;
3.51 to 5.0   previously at 0x28
union {
    LIST_ENTRY WaitListEntry;
    SINGLE_LIST_ENTRY SwapListEntry;
5.1 only   next at 0x60
0x60 (3.51);
0x64 (4.0 to 5.0);
ULONG WaitTime;
3.51 to 5.1   previously at 0x0180;
next at 0x6C
0x64 (3.51);
0x68 (4.0 to 5.0);
CHAR BasePriority;
3.51 to 5.1   previously at 0x01A9;
next at 0x0110
0x65 (3.51);
0x69 (4.0 to 5.0);
UCHAR DecrementCount;
3.51 to 5.1   previously at 0x019F
0x66 (3.51);
0x6A (4.0 to 5.0);
CHAR PriorityDecrement;
3.51 to 5.1   previously at 0x01AA;
next at 0x0112
0x67 (3.51);
0x6B (4.0 to 5.0);
CHAR Quantum;
3.51 to 5.1   previously at 0x01AB;
next at 0x0113
0x68 (3.51);
0x6C (4.0 to 5.0);
KWAIT_BLOCK WaitBlock [5];
3.51 only   previously at 0xA4
KWAIT_BLOCK WaitBlock [4];
4.0 to 5.1   next at 0xA0

A KWAIT_BLOCK is needed for each dispatcher object that a thread waits on. That callers of KeWaitForSingleObject aren’t asked for one is because the KTHREAD has its own. That callers of KeWaitForMultipleObjects typically don’t have to provide an array of wait blocks for the multiple objects is because the KTHREAD has not just one but several. The first three in the WaitBlock array have this purpose. Starting with the NTDDK.H from the Device Driver Kit (DDK) for Windows NT 4.0, this number of what a comment calls “Builtin usable wait blocks” is defined as THREAD_WAIT_OBJECTS. The comment is remarkably succinct: those three are not all of the built-in wait blocks, just the ones that are usable by callers who do not provide their own.

The last of the built-in wait blocks is dedicated to the thread’s own Timer (see below), for use as an implied addition to the objects that are being waited on when a timeout is specified.

Before version 4.0, the KTHREAD had yet another built-in wait block. The one whose 0-based index is 3 are in these ancient versions dedicated to synchronising a client and a server through an event-pair object. This type of kernel object is simple enough in general, though it seems never to have been formally documented. Two user-mode threads—you might call them client and server—create or open a pair of synchronisation events as one object by calling the NTDLL functions NtCreateEventPair and NtOpenEventPair. The two events—call them low and high—each represent one thread’s work. When a thread completes work for the other, it signals its event and waits on the other’s. They each do this as one call to the kernel, passing one handle to the NTDLL functions NtSetLowWaitHighEventPair and NtSetHighWaitLowEventPair. In version 5.0 and higher, once this operation gets to the kernel and the handles are resolved to objects, the kernel actually does just call KeSetEvent and KeWaitForSingleObject. Earlier versions, however, look for efficiency from the certainty that setting the event is just the first operation in a pair. They even give each thread a built-in event pair—though in the ETHREAD not the KTHREAD—that a client and server can operate through the NTDLL functions NtSetLowWaitHighThread and NtSetHighWaitLowThread without the overhead of interpreting a handle. The original Windows versions apparently regarded this as so important that these functions get to the kernel through their own interrupt numbers (0x2B and 0x2C), sparing the overhead of having the kernel look up its service table. Though this special attention given to synchronising with event pairs is arguably nothing but dim prehistory now, one formal vestige remains to this day in NTSTATUS.H where comments for STATUS_NO_EVENT_PAIR talk of a “thread specific client/server event pair object”.

The reduction of the WaitBlock array from five KWAIT_BLOCK structures to four in 4.0 seems to have been treated as an opportunity to insert (mostly) new members.

Offset (x86) Definition Versions Remarks Future
0xCC (4.0 to 5.0);
PVOID LegoData;
4.0 to 5.1   next at 0x01A8
0xD0 (4.0 to 5.0);
ULONG KernelApcDisable;
4.0 to 5.1 previously as UCHAR at 0x0134 next at 0x70
0xD4 (4.0 to 5.0);
KAFFINITY UserAffinity;
4.0 to 5.1   next at 0x0118
0xD8 (4.0 to 5.0);
BOOLEAN SystemAffinityActive;
4.0 to 5.1   next at 0x0114
0xD9 (5.0);
UCHAR PowerState;
5.0 to 5.1   next at 0x01B5
0xDA (5.0);
KIRQL NpxIrql;
5.0 to 5.1   next at 0x01B6
0xD9 (4.0);
0xDB (5.0);
UCHAR Pad [3];
4.0 only    
UCHAR Pad [1];
5.0 only    
UCHAR InitialNode;
5.1 only    
0xDC (4.0 to 5.0);
PVOID ServiceTable;
4.0 to 5.1 previously at 0x0124 next at 0x0124

Note that the preceding insertions for version 4.0 don’t fully reclaim the deleted KWAIT_BLOCK. It’s almost as if an effort was made to keep Queue at offset 0xE0, not that it stayed there long.

Offset (x86) Definition Versions Remarks Future
0xE0 (3.51 to 5.0);
KQUEUE *Queue;
3.51 to 5.1   next at 0x68
0xE4 (3.51 to 5.0);
apparently unused 4 bytes 3.51 only    
ULONG ApcQueueLock;
4.0 to 5.1   next at 0x4C
0xE8 (3.51 to 5.0);
3.51 to 5.1   previously at 0x38;
next at 0x78
0x0110 (3.51 to 5.0);
LIST_ENTRY QueueListEntry;
3.51 to 5.1   next at 0x0100
ULONG SoftAffinity;
5.1 only    
0x0118 (3.51 to 5.0);
3.51 to 5.1   previously at 0x0188;
next at 0x0120
0x011C (3.51 to 5.0);
BOOLEAN Preempted;
3.51 to 5.1   previously at 0x019A;
next at 0x010A
0x011D (3.51 to 5.0);
BOOLEAN ProcessReadyQueue;
3.51 to 5.1   previously at 0x019B;
next at 0x010B
0x011E (3.51 to 5.0);
BOOLEAN KernelStackResident;
3.51 to 5.1   previously at 0x019C;
next at 0x010C
0x011F (3.51 to 5.0);
UCHAR NextProcessor;
3.51 to 5.1   previously at 0x01A0;
next at 0x010F
0x0120 (3.51 to 5.0);
PVOID CallbackStack;
3.51 to 5.1   next at 0x0148
0x0124 (3.51 to 5.0);
PVOID ServiceTable;
3.51 only next at 0xDC  
PVOID Win32Thread;
5.0 to 5.1   next at 0x014C
0x0128 (3.51 to 5.0);
3.51 to 5.1   next at 0x0150
0x012C (3.51 to 5.0);
KAPC_STATE *ApcStatePointer [2];
3.51 to 5.1   previously at 0x0160;
next at 0x0128
0x0134 (3.51)
UCHAR KernelApcDisable;
3.51 only next as ULONG at 0xD0 previously at 0x01AE
0x0134 (5.0);
5.0 to 5.1 previously at 0x0137 next at 0x0115
0x0134 (4.0);
0x0135 (5.0);
BOOLEAN EnableStackSwap;
4.0 to 5.1   next at 0x5C
0x0135 (3.51 to 4.0);
0x0136 (5.0);
BOOLEAN LargeStack;
3.51 to 5.1   next at 0x01B4
0x0136 (3.51 to 4.0);
0x0137 (5.0);
apparently unused byte 3.51 only    
UCHAR ResourceIndex;
4.0 to 5.1   next at 0x0115
0x0137 (3.51 to 4.0)
3.10 to 4.0 next at 0x0134 previously at 0x01A8
0x0138 (3.51 to 5.0);
ULONG KernelTime;
3.51 to 5.1   previously at 0x30;
next at 0x0154
0x013C (3.51 to 5.0);
ULONG UserTime;
3.51 to 5.1   previously at 0x34;
next at 0x0158
0x0140 (3.51 to 5.0);
KAPC_STATE SavedApcState;
3.51 to 5.1   previously at 0x0148;
next at 0x0130
0x0158 (3.51 to 5.0);
BOOLEAN Alertable;
3.51 to 5.1   previously at 0x0194;
next at 0x58
0x0159 (3.51 to 5.0);
UCHAR ApcStateIndex;
3.51 to 5.1   previously at 0x019E;
next at 0x0108
0x015A (3.51 to 5.0);
BOOLEAN ApcQueueable;
3.51 to 5.1   previously at 0x0197;
next at 0x0109
0x015B (3.51 to 5.0);
BOOLEAN AutoAlignment;
3.51 to 5.1   previously at 0x0198;
next at 0x01B8
0x015C (3.51 to 5.0);
PVOID StackBase;
3.51 to 5.1   next at 0x015C
0x0160 (3.51 to 5.0);
KAPC SuspendApc;
3.51 to 5.1   previously at 0x60;
next at 0x0160
0x0190 (3.51 to 5.0);
KSEMAPHORE SuspendSemaphore;
3.51 to 5.1   previously at 0x90;
next at 0x0190
0x01A4 (3.51 to 5.0);
LIST_ENTRY ThreadListEntry;
3.51 to 5.1   previously at 0x20;
next at 0x01AC
0x01AC (3.51 to 5.0);
CHAR FreezeCount;
3.51 to 5.1   previously at 0x01A3;
next at 0x01BA
0x01AD (3.51 to 5.0);
CHAR SuspendCount;
3.51 to 5.1 last member in 3.51 previously at 0x01A4;
next at 0x01BB
0x01AE (4.0 to 5.0);
UCHAR IdealProcessor;
4.0 to 5.1   next at 0x010E
0x01AF (4.0 to 5.0);
BOOLEAN DisableBoost;
4.0 to 5.1 last member in 4.0;
last member in 5.0;
last member in 5.1
next at 0x0117