Geoff Chappell, Software Analyst
CURRENT WORK ITEM - PREVIEW ONLY
The Thread Environment Block is a thread’s user-mode representation. It has the highest-level knowledge of a thread in kernel mode and the lowest-level in user mode. If a thread has no user-mode footprint, it has no TEB. If only in principle, if anything about a thread can be properly managed in user mode without needing a transition to kernel mode, it goes in the TEB.
Code executing in user mode can easily find the TEB for the current thread because the fs or gs register, for 32-bit and 64-bit code respectively, holds a selector for a segment whose base address is that of the TEB. By reading just the NtTib.Self member with respect to the segment register, the user-mode code gets a linear address for convenient access to all the rest of the TEB. The 32-bit NTDLL even exports a function, named NtCurrentTeb and declared in WINNT.H, which does exactly this. For 64-bit Windows, NtCurrentTeb is a macro.
It is similarly easy for kernel-mode code to find the TEB for the current thread (if it has one). In kernel mode, the fs and gs registers, again for 32-bit and 64-bit code respectively, select a segment whose base address is that of a KPCR. Though this structure represents the current processor, not the current thread, it too has an NtTib member. Though it might be thought, aided by comments in NTDDK.H, that the Self member in NtTib would point back to NtTib, i.e., to the NT_TIB at the beginning of the processor’s KPCR, the NT_TIB it points to is in fact the one at the beginning of the TEB for the processor’s current thread.
That said, although at every switch of a processor to another thread the kernel updates the KPCR so that its NtTib.Self points to the incoming thread’s TEB, and notwithstanding what the kernel itself often does, finding the current TEB this way is surely not what Microsoft prefers in general. Each KTHREAD, which is the kernel’s representation of a thread, has a Teb member which, of course, holds the address of the thread’s TEB (again, if it has one). Since version 5.1, the kernel exports a function, named PsGetThreadTeb, that does this lookup for an arbitrary thread. Although a PsGetCurrentThreadTeb has to wait for the build of version 5.2 from Windows Server 2003 SP1, it too gets the TEB from the Teb in the current thread’s KTHREAD (from which it also checks that the thread is not a system thread and is not presently attached to the address space of another process).
In an ideal world, the TEB might be opaque, or even unknown, outside the kernel, NTDLL and perhaps a handful of other modules that are obviously at the very heart of Windows. The fs and gs registers are well-known as addressing an NT_TIB in user mode. This structure is defined in NTDDK.H and WINNT.H. A comment in NTDDK.H notes that the NT_TIB “appears as the first part of the TEB for all threads which have a user mode component” and WINNT.H either declares as a function or defines as a macro something named NtCurrentTeb which produces the address of the current thread’s TEB.
This could be the end of it, yet the TEB is not entirely opaque. Various high-level modules supplied with Windows over the years have used a few members of the TEB, and this eventually had to be disclosed. A new header, named WINTERNL.H, for previously internal APIs was added to the Software Development Kit (SDK) apparently in 2002, and remains to this day. It originally presented a modified TEB that has just the TlsSlots, ReservedForOle and TlsExpansionSlots members, plus padding that gets these members to the same offsets as in the true structure. It seems unlikely that Microsoft will change the TEB in any way that moves these members.
The three members defined in WINTERNL.H may be the only ones that have yet been disclosed for regulatory compliance, but others are known to modules even as far out as Internet Explorer (such that the use surely ought to have compelled disclosure). For instance, it takes mere seconds to establish that IEFRAME knows of the ProcessEnvironmentBlock member. That it got missed is plausibly because of an overlooked macro. If it is an oversight, it’s put right for WINTRNL.H in the SDK for Windows 8.
Other high-level knowledge, though not subject to disclosure, is clearly deliberate. For instance, the 32-bit SHELL32 and SHLWAPI know the special meaning that GdiBatchCount, almost at the end of the structure, has for 32-bit code running on 64-bit Windows. At the lower levels of the Win32 subsystem, very many modules written by Microsoft know of this or that in the TEB.
With such widespread dependencies, it should not surprise that the TEB is highly stable across Windows versions. It must almost certainly stay so for many versions yet. With very few exceptions, the TEB has varied only by extension rather than redefinition. The following changes of size are known:
Version | Size (x86) | Size (x64) |
---|---|---|
3.51 | 0x0F28 | |
4.0 | 0x0F88 | |
5.0 | 0x0FA4 | |
early 5.1 (before Windows XP SP2) | 0x0FB4 | |
late 5.1 (Windows XP SP2 and higher) | 0x0FB8 | |
early 5.2 (before Windows Server 2003 SP1) | 0x0FB8 | |
late 5.2 (Windows Server 2003 SP1 and higher) | 0x0FBC | 0x17D8 |
6.0 | 0x0FF8 | 0x1828 |
6.1 | 0x0FE4 | 0x1818 |
6.2 to 6.3 | 0x0FE8 | 0x1820 |
10.0 | 0x1000 | 0x1838 |
These sizes, and the offsets, types and names in the tables that follow, are from Microsoft’s symbol files for the kernel and NTDLL starting with Windows XP, but are something of a guess for earlier versions since the symbol files for these do not contain type information for the TEB. What’s known of Microsoft’s names and types for earlier versions is instead inferred from what use NTOSKRNL and NTDLL are seen to make of the TEB. Exhaustively tracking down all such use would be difficult, if not impossible, even with source code.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x00 | 0x00 |
NT_TIB NtTib; |
3.51 and higher |
0x1C | 0x38 |
PVOID EnvironmentPointer; |
3.51 and higher |
0x20 | 0x40 |
CLIENT_ID ClientId; |
3.51 and higher |
0x28 | 0x50 |
PVOID ActiveRpcHandle; |
3.51 and higher |
0x2C | 0x58 |
PVOID ThreadLocalStoragePointer; |
3.51 and higher |
0x30 | 0x60 |
PEB *ProcessEnvironmentBlock; |
3.51 and higher |
0x34 | 0x68 |
ULONG LastErrorValue; |
3.51 and higher |
0x38 | 0x6C |
ULONG CountOfOwnedCriticalSections; |
3.51 and higher |
0x3C | 0x70 |
PVOID CsrClientThread; |
3.51 and higher |
0x40 | 0x78 |
PVOID Win32ThreadInfo; |
3.51 and higher |
0x44 | 0x80 |
ULONG User32Reserved [0x1A]; |
3.51 and higher |
0xAC | 0xE8 |
ULONG UserReserved [5]; |
3.51 and higher |
0xC0 | 0x0100 |
PVOID WOW32Reserved; |
3.51 and higher |
0xC4 | 0x0108 |
ULONG CurrentLocale; |
3.51 and higher |
0xC8 | 0x010C |
ULONG FpSoftwareStatusRegister; |
3.51 and higher |
A relatively large reservation, for system use according to the name, gets partly reassigned for Windows 10 but only to change what it’s reserved for.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0xCC | 0x0110 |
PVOID SystemReserved1 [0x36]; |
3.51 and higher |
PVOID ReservedForDebuggerInstrumentation [0x10]; |
10.0 and higher | ||
0x010C | 0x0190 |
PVOID SystemReserved1 [0x26]; |
10.0 and higher |
Yes, there is a plan to write something here.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x01A4 | 0x02C0 |
LONG ExceptionCode; |
3.51 and higher |
0x02C4 |
UCHAR Padding0 [4]; |
6.3 and higher |
When activation contexts were introduced for Windows XP, the TEB had a whole ACTIVATION_CONTEXT_STACK carved from previously spare bytes.
Offset (x86) | Definition | Versions |
---|---|---|
0x01A8 (3.51 to early 5.2) |
UCHAR SpareBytes1 [0x2C]; |
3.51 to 5.0 |
ACTIVATION_CONTEXT_STACK ActivationContextStack; |
5.1 to early 5.2 | |
0x01BC (3.51 to early 5.2) |
UCHAR SpareBytes1 [0x18]; |
3.51 to early 5.2 |
In the Windows versions that have both x86 and x64 builds, the TEB has just a pointer to an ACTIVATION_CONTEXT_STACK. At first, this just returned almost all the bytes of the ACTIVATION_CONTEXT_STACK to being spare. Except that Windows Vista defined a member at the end, these spare bytes remained spare until Windows 10 put some to use for the instrumentation callback (that can be set through the ProcessInstrumentationCallback case of NtSetInformationProcess).
Offset (x86) | Definition | Versions |
---|---|---|
0x01A8 |
ACTIVATION_CONTEXT_STACK *ActivationContextStackPointer; |
late 5.2 and higher |
0x01AC |
UCHAR SpareBytes1 [0x28]; |
late 5.2 only |
UCHAR SpareBytes1 [0x24]; |
6.0 only | |
UCHAR SpareBytes [0x24]; |
6.1 to 6.3 | |
ULONG_PTR InstrumentationCallbackSp; |
10.0 and higher | |
0x01B0 |
ULONG_PTR InstrumentationCallbackPreviousPc; |
10.0 and higher |
0x01B4 |
ULONG_PTR InstrumentationCallbackPreviousSp; |
10.0 and higher |
0x01B8 |
BOOLEAN InstrumentationCallbackDisabled; |
10.0 and higher |
0x01B9 |
UCHAR SpareBytes [0x17]; |
10.0 and higher |
0x01D0 |
ULONG TxFsContext; |
6.0 and higher |
It may be just coincidental that the 64-bit TEB places the GdiTebBatch member (see below) exactly right for a whole ACTIVATION_CONTEXT_STACK. However, all known x64 builds have just a pointer to the ACTIVATION_CONTEXT_STACK and then spare bytes that get reduced and renamed until all that’s left of them is padding caused by an alignment requirement. Note that the members that are added for Windows 10 have a slightly different order for the different processors. The x86 builds keep all four members together, again carving them from the beginning of previously spare bytes, and leaving some still spare. Space is tighter in the x64 builds, such that the single-byte InstrumentationCallbackDisabled only fits by squeezing into the alignment requirement after the older TxFsContext.
Offset (x64) | Definition | Versions |
---|---|---|
0x02C8 |
ACTIVATION_CONTEXT_STACK *ActivationContextStackPointer; |
late 5.2 and higher |
0x02D0 |
UCHAR SpareBytes1 [0x1C]; |
late 5.2 only |
UCHAR SpareBytes1 [0x18]; |
6.0 only | |
UCHAR SpareBytes [0x18]; |
6.1 to 6.3 | |
ULONG_PTR InstrumentationCallbackSp; |
10.0 and higher | |
0x02D8 |
ULONG_PTR InstrumentationCallbackPreviousPc; |
10.0 and higher |
0x02E0 |
ULONG_PTR InstrumentationCallbackPreviousSp; |
10.0 and higher |
0x02E8 |
ULONG TxFsContext; |
6.0 and higher |
0x02EC |
UCHAR Padding1 [4]; |
6.3 and higher |
BOOLEAN InstrumentationCallbackDisabled; |
10.0 and higher | |
0x02ED |
UCHAR Padding1 [3]; |
6.3 and higher |
The remainder of the original TEB is stable. Note the GDI_TEB_BATCH structure which has always been a very large contributor to the size of the TEB.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x01D4 | 0x02F0 |
GDI_TEB_BATCH GdiTebBatch; |
3.51 and higher |
0x06B4 | 0x07D8 |
CLIENT_ID RealClientId; |
3.51 and higher |
0x06BC | 0x07E8 |
PVOID GdiCachedProcessHandle; |
3.51 and higher |
0x06C0 | 0x07F0 |
ULONG GdiClientPID; |
3.51 and higher |
0x06C4 | 0x07F4 |
ULONG GdiClientTID; |
3.51 and higher |
0x06C8 | 0x07F8 |
PVOID GdiThreadLocalInfo; |
3.51 and higher |
0x06CC | 0x0800 |
ULONG_PTR Win32ClientInfo [0x3E]; |
3.51 and higher |
0x07C4 | 0x09F0 |
PVOID glDispatchTable [0xE9]; |
3.51 and higher |
0x0B68 | 0x1138 |
ULONG_PTR glReserved1 [0x1D]; |
3.51 and higher |
0x0BDC | 0x1220 |
PVOID glReserved2; |
3.51 and higher |
0x0BE0 | 0x1228 |
PVOID glSectionInfo; |
3.51 and higher |
0x0BE4 | 0x1230 |
PVOID glSection; |
3.51 and higher |
0x0BE8 | 0x1238 |
PVOID glTable; |
3.51 and higher |
0x0BEC | 0x1240 |
PVOID glCurrentRC; |
3.51 and higher |
0x0BF0 | 0x1248 |
PVOID glContext; |
3.51 and higher |
0x0BF4 | 0x1250 |
ULONG LastStatusValue; |
3.51 and higher |
0x0BF8 | 0x1258 |
UNICODE_STRING StaticUnicodeString; |
3.51 and higher |
0x0C00 | 0x1268 |
WCHAR StaticUnicodeBuffer [0x0105]; |
3.51 and higher |
0x1472 |
UCHAR Padding3 [6]; |
6.3 and higher | |
0x0E0C | 0x1478 |
PVOID DeallocationStack; |
3.51 and higher |
0x0E10 | 0x1480 |
PVOID TlsSlots [0x40]; |
3.51 and higher |
0x0F10 | 0x1680 |
LIST_ENTRY TlsLinks; |
3.51 and higher |
0x0F18 | 0x1690 |
PVOID Vdm; |
3.51 and higher |
0x0F1C | 0x1698 |
PVOID ReservedForNtRpc; |
3.51 and higher |
0x0F20 | 0x16A0 |
PVOID DbgSsReserved [2]; |
3.51 and higher |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0F28 | 0x16B0 |
ULONG HardErrorsAreDisabled; |
4.0 to 5.1 |
ULONG HardErrorMode; |
5.2 and higher |
Yes, there is a plan to write something here.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0F2C |
PVOID Instrumentation [0x10]; |
4.0 to early 5.2 | |
0x16B8 |
PVOID Instrumentation [0x0E]; |
late 5.2 only | |
PVOID Instrumentation [0x09]; |
6.0 and higher (x86) | ||
PVOID Instrumentation [0x0B]; |
6.0 and higher (x64) | ||
0x0F50 | 0x1710 |
GUID ActivityId; |
6.0 and higher |
0x0F64 (late 5.2); 0x0F60 |
0x1728 (late 5.2); 0x1720 |
PVOID SubProcessTag; |
late 5.2 and higher |
0x0F64 | 0x1728 |
PVOID EtwLocalData; |
6.0 to 6.1 |
PVOID PerflibData; |
6.2 and higher | ||
0x0F68 | 0x1730 |
PVOID EtwTraceData; |
late 5.2 and higher |
Yes, there is a plan to write something here.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0F6C | 0x1738 |
PVOID WinSockData; |
4.0 and higher |
0x0F70 | 0x1740 |
ULONG GdiBatchCount; |
4.0 and higher |
0x0F74 | 0x1744 |
BOOLEAN InDbgPrint; |
4.0 to 5.2 |
BOOLEAN SpareBool0; |
6.0 only | ||
union { PROCESSOR_NUMBER CurrentIdealProcessor; ULONG IdealProcessorValue; struct { UCHAR ReservedPad0; UCHAR ReservedPad1; UCHAR ReservedPad2; UCHAR IdealProcessor; }; }; |
6.1 and higher | ||
0x0F75 | 0x1745 |
BOOLEAN FreeStackOnTermination; |
4.0 to 5.2 |
BOOLEAN SpareBool1; |
6.0 only | ||
0x0F76 | 0x1746 |
BOOLEAN HasFiberData; |
4.0 to 5.2 |
BOOLEAN SpareBool2; |
6.0 only | ||
0x0F77 | 0x1747 |
UCHAR IdealProcessor; |
4.0 to 6.0 |
0x0F78 | 0x1748 |
ULONG Spare3; |
4.0 to early 5.2 |
ULONG GuaranteedStackBytes; |
late 5.2 and higher | ||
0x174C |
UCHAR Padding5 [4]; |
6.3 and higher | |
0x0F7C | 0x1750 |
PVOID ReservedForPerf; |
4.0 and higher |
0x0F80 | 0x1758 |
PVOID ReservedForOle; |
4.0 and higher |
0x0F84 | 0x1760 |
ULONG WaitingOnLoaderLock; |
4.0 and higher |
0x1764 |
UCHAR Padding6 [4]; |
6.3 and higher |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0F88 |
struct _Wx86ThreadState { ULONG *CallBx86Eip; PVOID DeallocationCpu; UCHAR UseKnownWx86Dll; CHAR OleStubInvoked; } Wx86Thread; |
5.0 to early 5.2 | |
0x1768 |
ULONG_PTR SparePointer1; |
late 5.2 only | |
PVOID SavedPriorityState; |
6.0 and higher | ||
0x0F8C | 0x1770 |
ULONG_PTR SoftPatchPtr1; |
late 5.2 to 6.1 |
ULONG_PTR ReservedForCodeCoverage; |
6.2 and higher | ||
0x0F90 | 0x1778 |
ULONG_PTR SoftPatchPtr2; |
late 5.2 only |
PVOID ThreadPoolData; |
6.0 and higher | ||
0x0F94 | 0x1780 |
PVOID *TlsExpansionSlots; |
5.0 and higher |
0x1788 |
PVOID DeallocationBStore; |
late 5.2 and higher | |
0x1790 |
PVOID BStoreLimit; |
late 5.2 and higher | |
0x0F98 | 0x1798 |
ULONG ImpersonationLocale; |
5.0 to 6.0 |
ULONG MuiGeneration; |
6.1 and higher | ||
0x0F9C | 0x179C |
ULONG IsImpersonating; |
5.0 and higher |
0x0FA0 | 0x17A0 |
PVOID NlsCache; |
5.0 and higher |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0FA4 | 0x17A8 |
PVOID pShimData; |
5.1 and higher |
0x0FA8 | 0x17B0 |
ULONG HeapVirtualAffinity; |
5.1 to 6.1 |
USHORT HeapVirtualAffinity; |
6.2 and higher | ||
0x0FAA | 0x17B2 |
USHORT LowFragHeapDataSlot; |
6.2 and higher |
0x17B4 |
UCHAR Padding7 [4]; |
6.3 and higher | |
0x0FAC | 0x17B8 |
PVOID CurrentTransactionHandle; |
5.1 and higher |
0x0FB0 | 0x17C0 |
TEB_ACTIVE_FRAME *ActiveFrame; |
5.1 and higher |
0x0FB4 | 0x17C8 |
PVOID FlsData; |
5.2 and higher |
0x0FB4 (late 5.1); 0x0FB8 (late 5.2) |
0x17D0 (late 5.2) |
BOOLEAN SafeThunkCall; |
late 5.1 and late 5.2 |
0x0FB5 (late 5.1); 0x0FB9 (late 5.2) |
0x17D1 (late 5.2) |
BOOLEAN BooleanSpare [3]; |
late 5.1 and late 5.2 |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0FB8 | 0x17D0 |
PVOID PreferredLanguages; |
6.0 and higher |
0x0FBC | 0x17D8 |
PVOID UserPrefLanguages; |
6.0 and higher |
0x0FC0 | 0x17E0 |
PVOID MergedPrefLanguages; |
6.0 and higher |
0x0FC4 | 0x17E8 |
ULONG MuiImpersonation; |
6.0 and higher |
0x0FC8 | 0x17EC |
union { USHORT volatile CrossTebFlags; struct { USHORT SpareCrossTebBits : 16; }; }; |
6.0 and higher |
0x0FCA | 0x17EE |
union { USHORT SameTebFlags; struct { /* bit fields, see below */ }; }; |
6.0 and higher |
0x0FCC | 0x17F0 |
PVOID TxnScopeEnterCallback; |
6.0 and higher |
0x0FD0 | 0x17F8 |
PVOID TxnScopeExitCallback; |
6.0 and higher |
0x0FD4 | 0x1800 |
PVOID TxnScopeContext; |
6.0 and higher |
0x0FD8 | 0x1808 |
ULONG LockCount; |
6.0 and higher |
The last few members were all discarded after Windows Vista, such that the TEB shrinks.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0FDC (6.0) | 0x180C |
ULONG ProcessRundown; |
6.0 only |
0x0FE0 (6.0) | 0x1810 |
ULONGLONG LastSwitchTime; |
6.0 only |
0x0FE8 (6.0) | 0x1820 |
ULONGLONG TotalSwitchOutTime; |
6.0 only |
0x0FF0 (6.0) | 0x1828 |
LARGE_INTEGER WaitReasonBitMap; |
6.0 only |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0FDC | 0x180C |
ULONG SpareUlong0; |
6.1 to 6.3 |
LONG WowTebOffset; |
10.0 and higher | ||
0x0FE0 | 0x1810 |
PVOID ResourceRetValue; |
6.1 and higher |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0FE4 | 0x1818 |
PVOID ReservedForWdf; |
6.2 and higher |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0FE8 | 0x1820 |
ULONGLONG ReservedForCrt; |
10.0 and higher |
0x0FF0 | 0x1828 |
GUID EffectiveContainerId; |
10.0 and higher |
Windows Vista introduced two sets of bit fields. Each is overlaid by a USHORT for simultaneous access to the bits.
The CrossTebFlags have been provided for but never defined: all bits are spare.
Except that Dbg and Rtl prefixes were removed for Windows 7, the SameTebFlags have changed only by addition:
Mask | Definition | Versions |
---|---|---|
0x0001 |
USHORT DbgSafeThunkCall : 1; |
6.0 only |
USHORT SafeThunkCall : 1; |
6.1 and higher | |
0x0002 |
USHORT DbgInDebugPrint : 1; |
6.0 only |
USHORT InDebugPrint : 1; |
6.1 and higher | |
0x0004 |
USHORT DbgHasFiberData : 1; |
6.0 only |
USHORT HasFiberData : 1; |
6.1 and higher | |
0x0008 |
USHORT DbgSkipThreadAttach : 1; |
6.0 only |
USHORT SkipThreadAttach : 1; |
6.1 and higher | |
0x0010 |
USHORT DbgWerInShipAssertCode : 1; |
6.0 only |
USHORT WerInShipAssertCode : 1; |
6.1 and higher | |
0x0020 |
USHORT DbgRanProcessInit : 1; |
6.0 only |
USHORT RanProcessInit : 1; |
6.1 and higher | |
0x0040 |
USHORT DbgClonedThread : 1; |
6.0 only |
USHORT ClonedThread : 1; |
6.1 and higher | |
0x0080 |
USHORT DbgSuppressDebugMsg : 1; |
6.0 only |
USHORT SuppressDebugMsg : 1; |
6.1 and higher | |
0x0100 |
USHORT RtlDisableUserStackWalk : 1; |
late 6.0 only |
USHORT DisableUserStackWalk : 1; |
6.1 and higher | |
0x0200 |
USHORT RtlExceptionAttached : 1; |
late 6.0 and higher |
0x0400 |
USHORT InitialThread : 1; |
6.1 and higher |
0x0800 |
USHORT SessionAware : 1; |
6.2 and higher |
0x1000 |
USHORT LoadOwner : 1; |
10.0 and higher |
0x2000 |
USHORT LoaderWorker : 1; |
10.0 and higher |
USHORT SpareSameTebBits : 8; |
early 6.0 only | |
USHORT SpareSameTebBits : 6; |
late 6.0 and higher | |
USHORT SpareSameTebBits : 5; |
6.1 only | |
USHORT SpareSameTebBits : 4; |
6.2 to 6.3 | |
USHORT SpareSameTebBits : 2; |
10,0 and higher |