SKETCH OF HOW RESEARCH MIGHT CONTINUE AND RESULTS BE PRESENTED

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.

Variability

The KTHREAD structure is plainly internal to the kernel and its layout changes greatly between Windows versions and even between builds. That said, it does not change much through the early Windows versions. The size hardly changes: 0x01B0 bytes until the first growth, to 0x01C0 bytes, for version 5.1. Within the structure, the early versions do have some reordering—for instance, KernelStack and Teb get swapped for version 4.0 (plausibly as a side-effect of inserting TlsArray)—but not so many to confound presentation. In the layout table below, a handful of members each make two appearances because of such relocations. 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 History column points the way to that member’s appearance in a separate table for the KTHREAD in early builds of version 5.2.

Layout

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 History
0x00
DISPATCHER_HEADER Header;
3.51 to 5.1   next at same
0x10
LIST_ENTRY MutantListHead;
3.51 to 5.1   next at same
0x18
PVOID InitialStack;
3.51 to 5.1   next at same
0x1C
PVOID StackLimit;
3.51 to 5.1   next at same
0x20 (3.51)
PVOID KernelStack;
3.51 only next at 0x28  
0x24 (3.51);
0x20
PVOID Teb;
3.51 to 5.1   next at 0x30
0x24
PVOID TlsArray;
4.0 to 5.1   next at 0x01A4
0x28
PVOID KernelStack;
4.0 to 5.1 previously at 0x20 (3.51) next at 0x20
0x28 (3.51);
0x2C
BOOLEAN DebugActive;
3.51 to 5.1   next at 0x03 in Header
0x29 (3.51);
0x2D
UCHAR State;
3.51 to 5.1   next at 0x2C
0x2A (3.51);
0x2E
BOOLEAN Alerted [MaximumMode];
3.51 to 5.1   next at 0x5E
0x2C (3.51);
0x30
UCHAR Iopl;
3.51 to 5.1   next at 0x01B9
0x2D (3.51);
0x31
UCHAR NpxState;
3.51 to 5.1   next at 0x2D
0x2E (3.51);
0x32
CHAR Saturation;
3.51 to 5.1   next at 0x010D
0x2F (3.51);
0x33
CHAR Priority;
3.51 to 5.1   next at 0x5B
0x30 (3.51);
0x34
KAPC_STATE ApcState;
3.51 to 5.1   next at same
0x48 (3.51);
0x4C
ULONG ContextSwitches;
3.51 to 5.1   next at 0x28
0x50
UCHAR IdleSwapBlock;
5.1 only    
0x51
UCHAR Spare0 [3];
5.1 only    
0x4C (3.51);
0x50 (4.0 to 5.0);
0x54
LONG WaitStatus;
3.51 to 5.1   next at 0x50
0x50 (3.51);
0x54 (4.0 to 5.0);
0x58
KIRQL WaitIrql;
3.51 to 5.1   next at 0x2E
0x51 (3.51);
0x55 (4.0 to 5.0);
0x59
KPROCESSOR_MODE WaitMode;
3.51 to 5.1   next at 0x2F
0x52 (3.51);
0x56 (4.0 to 5.0);
0x5A
BOOLEAN WaitNext;
3.51 to 5.1   next at 0x59
0x53 (3.51);
0x57 (4.0 to 5.0);
0x5B
UCHAR WaitReason;
3.51 to 5.1   next at 0x5A
0x54 (3.51);
0x58 (4.0 to 5.0);
0x5C
KWAIT_BLOCK *WaitBlockList;
3.51 to 5.1   next at 0x54
0x58 (3.51);
0x5C (4.0 to 5.0);
0x60
LIST_ENTRY WaitListEntry;
3.51 to 5.0    
union {
    LIST_ENTRY WaitListEntry;
    SINGLE_LIST_ENTRY SwapListEntry;
};
5.1 only   next at 0x60
0x60 (3.51);
0x64 (4.0 to 5.0);
0x68
ULONG WaitTime;
3.51 to 5.1   next at 0x6C
0x64 (3.51);
0x68 (4.0 to 5.0);
0x6C
CHAR BasePriority;
3.51 to 5.1   next at 0x0110
0x65 (3.51);
0x69 (4.0 to 5.0);
0x6D
UCHAR DecrementCount;
3.51 to 5.1    
0x66 (3.51);
0x6A (4.0 to 5.0);
0x6E
CHAR PriorityDecrement;
3.51 to 5.1   next at 0x0112
0x67 (3.51);
0x6B (4.0 to 5.0);
0x6F
CHAR Quantum;
3.51 to 5.1   next at 0x0113
0x68 (3.51);
0x6C (4.0 to 5.0);
0x70
KWAIT_BLOCK WaitBlock [5];
3.51 only    
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.

Version 3.51 has an extra built-in wait block. The one whose 0-based index is 3 is in this version 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. 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 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. Version 3.51 apparently regarded this as so important that these functions get to the kernel through their own interrupt numbers (0x2B and 0x2C): synchronisation with the built-in event-pair is even spared the overhead of the kernel looking up its service table.

The special attention given to synchronising with event pairs is perhaps nothing but dim prehistory now. One formal vestige is 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 in version 3.51 to four in 4.0 seems to have been treated as leaving space—0x18 bytes—for new members.

Offset (x86) Definition Versions Remarks History
0xCC (4.0 to 5.0);
0xD0
PVOID LegoData;
4.0 to 5.1   next at 0x01A8
0xD0 (4.0 to 5.0);
0xD4
ULONG KernelApcDisable;
4.0 to 5.1 previously as UCHAR at 0x0134 (3.51) next at 0x70
0xD4 (4.0 to 5.0);
0xD8
KAFFINITY UserAffinity;
4.0 to 5.1   next at 0x0118
0xD8 (4.0 to 5.0);
0xDC
BOOLEAN SystemAffinityActive;
4.0 to 5.1   next at 0x0114
0xD9 (4.0);
0xD9 (5.0);
0xDD
apparently unused 7 bytes 4.0 only    
UCHAR PowerState;
5.0 to 5.1   next at 0x01B5
0xDA (5.0);
0xDE
KIRQL NpxIrql;
5.0 to 5.1   next at 0x01B6
0xDB (5.0);
0xDF
UCHAR Pad [1];
5.0 only    
UCHAR InitialNode;
5.1 only    

For version 5.0 to record the address of the Win32Thread for WIN32K.SYS separately from the ServiceTable, the latter was inserted with the other new members even though the space that had been left was by then all in use but for one byte.

Offset (x86) Definition Versions Remarks History
0xDC (5.0);
0xE0
PVOID ServiceTable;
5.0 to 5.1 previously at 0x0124 (3.51 to 4.0) next at 0x0124
0xE0 (3.51 to 5.0);
0xE4
KQUEUE *Queue;
3.51 to 5.1   next at 0x68
0xE4 (3.51 to 5.0);
0xE8
apparently unused 4 bytes 3.51 only    
ULONG ApcQueueLock;
4.0 to 5.1   next at 0x4C
0xE8 (3.51 to 5.0);
0xF0
KTIMER Timer;
3.51 to 5.1   next at 0x78
0x0110 (3.51 to 5.0);
0x0118
LIST_ENTRY QueueListEntry;
3.51 to 5.1   next at 0x0100
0x0120
ULONG SoftAffinity;
5.1 only    
0x0118 (3.51 to 5.0);
0x0124
KAFFINITY Affinity;
3.51 to 5.1   next at 0x0120
0x011C (3.51 to 5.0);
0x0128
BOOLEAN Preempted;
3.51 to 5.1   next at 0x010A
0x011D (3.51 to 5.0);
0x0129
BOOLEAN ProcessReadyQueue;
3.51 to 5.1   next at 0x010B
0x011E (3.51 to 5.0);
0x012A
BOOLEAN KernelStackResident;
3.51 to 5.1   next at 0x010C
0x011F (3.51 to 5.0);
0x012B
UCHAR NextProcessor;
3.51 to 5.1   next at 0x010F
0x0120 (3.51 to 5.0);
0x012C
PVOID CallbackStack;
3.51 to 5.1   next at 0x0148
0x0124 (3.51 to 5.0);
0x0130
PVOID ServiceTable;
3.51 to 4.0 next at 0xDC (5.0)  
PVOID Win32Thread;
5.0 to 5.1   next at 0x014C
0x0128 (3.51 to 5.0);
0x0134
KTRAP_FRAME *TrapFrame;
3.51 to 5.1   next at 0x0150
0x012C (3.51 to 5.0);
0x0138
KAPC_STATE *ApcStatePointer [2];
3.51 to 5.1   next at 0x0128
0x0134 (3.51)
UCHAR KernelApcDisable;
3.51 only next as ULONG at 0xD0 (4.0 to 5.0)  
0x0134 (5.0);
0x0140
KPROCESSOR_MODE PreviousMode;
5.0 to 5.1 previously at 0x0137 (3.51 to 4.0) next at 0x0115
0x0134 (4.0);
0x0135 (5.0);
0x0141
BOOLEAN EnableStackSwap;
4.0 to 5.1   next at 0x5C
0x0135 (3.51 to 4.0);
0x0136 (5.0);
0x0142
BOOLEAN LargeStack;
3.51 to 5.1   next at 0x01B4
0x0136 (3.51 to 4.0);
0x0137 (5.0);
0x0143
apparently unused byte 3.51 only    
UCHAR ResourceIndex;
4.0 to 5.1   next at 0x0115
0x0137 (3.51 to 4.0)
KPROCESSOR_MODE PreviousMode;
3.51 to 4.0 next at 0x0134 (5.0)  
0x0138 (3.51 to 5.0);
0x0144
ULONG KernelTime;
3.51 to 5.1   next at 0x0154
0x013C (3.51 to 5.0);
0x0148
ULONG UserTime;
3.51 to 5.1   next at 0x0158
0x0140 (3.51 to 5.0);
0x014C
KAPC_STATE SavedApcState;
3.51 to 5.1   next at 0x0130
0x0158 (3.51 to 5.0);
0x0164
BOOLEAN Alertable;
3.51 to 5.1   next at 0x58
0x0159 (3.51 to 5.0);
0x0165
UCHAR ApcStateIndex;
3.51 to 5.1   next at 0x0108
0x015A (3.51 to 5.0);
0x0166
BOOLEAN ApcQueueable;
3.51 to 5.1   next at 0x0109
0x015B (3.51 to 5.0);
0x0167
BOOLEAN AutoAlignment;
3.51 to 5.1   next at 0x01B8
0x015C (3.51 to 5.0);
0x0168
PVOID StackBase;
3.51 to 5.1   next at 0x015C
0x0160 (3.51 to 5.0);
0x016C
KAPC SuspendApc;
3.51 to 5.1   next at 0x0160
0x0190 (3.51 to 5.0);
0x019C
KSEMAPHORE SuspendSemaphore;
3.51 to 5.1   next at 0x0190
0x01A4 (3.51 to 5.0);
0x01B0
LIST_ENTRY ThreadListEntry;
3.51 to 5.1   next at 0x01AC
0x01AC (3.51 to 5.0);
0x01B8
CHAR FreezeCount;
3.51 to 5.1   next at 0x01BA
0x01AD (3.51 to 5.0);
0x01B9
CHAR SuspendCount;
3.51 to 5.1 last member in 3.51 next at 0x01BB
0x01AE (4.0 to 5.0);
0x01BA
UCHAR IdealProcessor;
4.0 to 5.1   next at 0x010E
0x01AF (4.0 to 5.0);
0x01BB
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