Threads, KTHREAD, and the Windows Scheduler

Windows Internals
Time it takes to read this article 6 minutes.

In Windows, the thread — not the process — is the fundamental unit of execution. A process is really just a container: it owns an address space, a handle table, a security token, and a set of resources, but it never runs code on its own. Execution belongs to the threads inside it. Each thread carries its own kernel and user stacks, its own saved register context (so it can be paused and resumed transparently), its own scheduling priority, and its own thread-local storage. When people say “Windows scheduled my program,” what the kernel actually scheduled was one of its threads onto a logical processor. This article dissects how a thread is represented in the kernel, the user-mode structures that accompany it, and how the dispatcher decides which thread runs next.

ETHREAD and KTHREAD

Every thread is described in kernel memory by an ETHREAD structure — the executive thread block. The first member of ETHREAD is a KTHREAD (named Tcb, the Thread Control Block), which is the kernel/dispatcher-level view of the thread. Because KTHREAD sits at offset 0, a pointer to an ETHREAD is also a valid pointer to its KTHREAD; the executive layer wraps the scheduler’s core structure with additional bookkeeping.

The ETHREAD holds identity and lifetime information that the executive cares about:

  • Cid — the Client ID, a pair of the owning process ID and the thread ID. This is how a thread is named system-wide.
  • ThreadListEntry — links the thread into its parent process’s list of threads (the EPROCESS.ThreadListHead).
  • StartAddress — the kernel-level start routine.
  • Win32StartAddress — the address the thread was actually created to run (the routine passed to CreateThread). This field is a favorite of both debuggers and threat hunters.

The embedded KTHREAD carries everything the scheduler touches on a hot path:

  • State — the current scheduling state (Running, Ready, Waiting, Standby, and so on).
  • Priority — the thread’s current scheduling priority (0–31).
  • KernelStack — pointer to the saved kernel stack for this thread.
  • Teb — pointer to the user-mode Thread Environment Block.
  • WaitBlock — an array of wait blocks describing the objects the thread is currently blocked on.
  • TrapFrame — pointer to the saved trap frame (the register snapshot captured when the thread entered the kernel).

The TEB

Complementing the kernel structures is the Thread Environment Block (TEB), which lives in user-mode memory and is directly accessible to the thread without a system call. On x64, the current thread’s TEB is reachable through the gs segment at gs:[0x30]; on x86 it is fs:[0x18]. Because it is per-thread and cheaply reachable, the TEB is where the runtime stashes fast-access state:

  • A pointer to the PEB (Process Environment Block), shared by all threads in the process.
  • Thread-Local Storage (TLS) slots, backing TlsAlloc/TlsGetValue.
  • The thread’s last-error value, returned by GetLastError.
  • The stack base and stack limit, defining the extent of the user-mode stack.
  • On x86, the ExceptionList — the head of the SEH (Structured Exception Handling) chain.

Reading gs:[0x30] to locate the TEB, then following it to the PEB, is one of the most common building blocks in shellcode, because it lets position-independent code find the process’s loaded modules without calling any API.

Thread States and Transitions

A thread moves through a well-defined set of scheduling states over its lifetime. The dispatcher drives these transitions:

  • Initialized — the thread is being created and is not yet eligible to run.
  • Ready — eligible to run and waiting in a per-processor ready queue for a processor to become available.
  • Standby — selected to run next on a specific processor. Only one thread per processor can be in Standby.
  • Running — currently executing on a processor.
  • Waiting — blocked on one or more dispatcher objects (an event, a mutex, an I/O completion, a timer).
  • Transition — ready to run, but its kernel stack is currently paged out and must be brought back in first.
  • DeferredReady — queued to be made ready, used to reduce lock contention on multiprocessor systems before the thread is placed on a specific processor’s ready queue.
  • Terminated — execution has finished and the structures are being torn down.

The common cycle is Ready → Standby → Running. A running thread that blocks on a wait goes to Waiting; when the wait is satisfied it becomes Ready (or DeferredReady, then Ready) again. A running thread that is preempted or exhausts its quantum returns directly to Ready.

Priorities

Windows scheduling is priority-driven, with 32 priority levels numbered 0 to 31. They divide into three bands:

  • Level 0 is reserved for the zero-page thread, which zeroes free physical pages in the background.
  • Levels 1–15 form the dynamic range used by normal applications. Threads here can have their priority temporarily boosted and decayed by the kernel.
  • Levels 16–31 are the real-time range. Priorities here are static — the kernel does not apply boosts — and a real-time thread will starve any lower-priority thread that is ready.

Each thread has a base priority derived from its process’s priority class and its own relative thread priority, and a current priority that the scheduler may raise above the base within the dynamic range. The kernel applies priority boosts in several situations: after an I/O operation completes (so the thread that was waiting on it runs promptly), when a wait on a dispatcher object is satisfied, when a thread in the foreground process is scheduled, and to combat priority inversion. After a boost, the current priority decays one level per quantum until it returns to base. Process priority classes — Idle, Below Normal, Normal, Above Normal, High, and Real-Time — map onto these base priority values.

The Dispatcher and Scheduling

The Windows scheduler — the dispatcher — is preemptive, priority-driven, and per-processor. There is no single global run queue on modern Windows; instead, each logical processor owns its own set of ready queues, stored in its KPRCB (Processor Control Block). The collection of scheduling state is referred to as the dispatcher database.

The core rule is simple: the highest-priority ready thread should always be running. When a thread becomes ready at a priority higher than a currently running thread, it preempts that thread. Absent preemption, a thread runs until it blocks, yields, or exhausts its quantum — the time slice it is granted. Quantum end triggers a rescheduling decision: the thread is placed back on the ready queue for its priority level, and the dispatcher selects the next thread to run. Each thread has an ideal processor and an affinity mask that constrain which processors it may run on, which the scheduler uses to preserve cache locality while still balancing load.

Scheduling is not a leisurely user-mode activity. Dispatch decisions and the manipulation of the dispatcher database occur at DISPATCH_LEVEL (IRQL 2). Code running at or above DISPATCH_LEVEL cannot be preempted by the scheduler and cannot block on dispatcher objects — which is exactly why deferred procedure calls (DPCs), which also run at this level, must be short and non-blocking.

Context Switching

When the dispatcher switches from one thread to another, it performs a context switch: it saves the outgoing thread’s volatile and non-volatile register state, its stack pointer, and its instruction pointer, and restores the same for the incoming thread. The saved state on kernel entry is captured in the TrapFrame; the deeper scheduler-level switch is handled inside the kernel’s Ki context-switch routines, which swap the kernel stack pointer and, if the incoming thread belongs to a different process, switch the address space by loading a new page-table base (CR3).

Two per-processor structures anchor all of this. The KPCR (Processor Control Region) is the root per-processor structure, reachable through the gs segment in kernel mode; embedded within it is the KPRCB holding the ready queues, the currently running thread pointer, the next (standby) thread, and the idle thread. The scheduler consults and updates these on every switch.

Inspecting Threads in WinDbg

All of these structures are directly observable in a kernel debugger. A few essential commands:

# Dump a process together with all its threads AND their stacks
!process <eprocess-addr> 7

# Examine the raw ETHREAD structure and its fields
dt nt!_ETHREAD <ethread-addr>

# Detailed view of a single thread: state, priority, wait reason, stack
!thread <ethread-addr>

# List threads in the current process context / show what each CPU is running
~
!running

!process <addr> 7 is the workhorse: the 7 flag adds the thread list and each thread’s call stack. dt nt!_ETHREAD reveals the field offsets — useful when you want to read Win32StartAddress or Cid directly. !thread decodes a single thread’s state, its base and current priority, and what dispatcher object it is waiting on. !running shows, per processor, which thread currently holds the CPU.

Security Relevance

Threads are a primary target for code injection and evasion because hijacking execution ultimately means hijacking a thread. Thread context manipulation — suspending a thread, rewriting its register context with SetThreadContext so the instruction pointer lands on attacker-controlled code, then resuming it — is the classic “thread hijacking” primitive. APC injection works differently: it queues a user-mode asynchronous procedure call onto a target thread, so the payload runs the next time that thread enters an alertable wait. Both techniques operate on the exact structures described above.

For defenders, the same fields become hunting signals. The Win32StartAddress of a legitimate thread almost always points into a module backed by a file on disk. A thread whose start address falls in private, executable, unbacked memory is a strong indicator of injected code — this is the basis of many “unbacked thread start” detections. Likewise, the TEB’s stack base and limit define the legitimate bounds of the user stack, which matters when reasoning about stack-based exploits and stack-pivot detection. Understanding thread internals is therefore as valuable to the blue team as it is to the red team.

Conclusion

The thread is where the abstract idea of “a running program” meets concrete kernel data structures: the ETHREAD/KTHREAD pair that the scheduler manipulates, the TEB that user code relies on, the state machine the dispatcher drives, and the priority model that decides who runs next. With per-processor ready queues, quantum-based preemption, and priority boosts, the dispatcher continuously enforces a single invariant — the highest-priority ready thread runs. Having seen how execution is represented and scheduled, the next article in the series turns to the space those threads execute in: Windows virtual memory management, page tables, and working sets.

You Might Also Like

This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:

Comments

Copied title and URL