Geoff Chappell, Software Analyst
SKETCH OF HOW RESEARCH MIGHT CONTINUE AND RESULTS BE PRESENTED
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. 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.
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 |