Win32 系统线程信息块(TIB)浅析

来源:岁月联盟 编辑:zhu 时间:2009-03-12

  原文出处:May 1996 Under The Hood

  Windows 操作系统各个版本之间虽然核心部分差异很大,但它们都共享一个关键的系统数据结构,许多程序员都没有加以关注。更精确地说,这种共享是针对此数据结构的某些域。先不说差别,这个结构被广泛使用,甚至是被编译器产生的代码存取。没错,你的C 编译器产生代码直接存取系统级的信息。

  到底是什么结构呢?这个结构至少有两个不同的名字。在 Windows 95 中称之为“线程信息块”(TIB:Thread Information Block)。在 Windows NT 中,它被称为“线程环境块”(TEB:Thread Environment Block)。但是,我看到在某些 Windows NT 都文件中也有叫 TIB 的,本文中我将它称之为 TIB 吧。

  线程信息块中到底有什么东西呢?让它如此特别。顾名思义,TIB 中的数据肯定与线程有关,并且在系统中每个线程都对应有一个 TIB。在 Windows 各个版本共享的数据域中,你会发现:有一个指针指向线程结构化异常处理列表,线程堆栈的位置以及线程局部存储槽。TIB 中的其它信息域在 Windows 各个版本之间是不同的。

  TIB 并不是在 Windows NT 或 Windows 95 第一次出现的,对此你可能会觉得奇怪。其实 TIB 的祖先是在 OS/2 中,在微软创建 Windows NT 之前就有了,并且至今都存在于 OS/2 中。事实上,TIB 在 OS/2 和 Windows 之间不仅格式一样,连存取方式都一样。在微软的头文件 NTDDK.H 中甚至有一行是这样写的:

<< begin snippet >>
// This structure MUST MATCH OS/2 V2.0!
<< end snippet >>

  光说不练是无法了解 TIB 的,其实就是 FS 寄存器指向的内容。嘿!Win32 中还有段和段寄存器吗?大多数情景是这样的,但都是在基于 Intel 平台的 Win32 系统中实现的(即使是其前身 Win32s),FS 寄存器指向 TIB。因此,我稍候要详细描述的所有结构偏移都适用于 FS 寄存器指向的段偏移。例如,FS:[0] 指向结构化异常处理链,而 FS:[2C] 指向线程的线程局部存储数组。

  我刚才提到了编译器直接存取 TIB 结构。让我们用一个小例子来看看这是怎么做的。这是一个使用结构化异常处理的很小的C程序:

 int main()
{

__try

{ int i = 0;


}

__except( 1 )

{ int j = 1: }



return 0;
}

  在编译的时候,最开始的结果代码是下面这个样子:

401000: PUSH EBP
401001: MOV EBP,ESP
401003: PUSH FF
401005: PUSH 00404000
40100A: PUSH 00401140
40100F: MOV EAX,FS:[00000000]
401015: PUSH EAX
401016: MOV DWORD PTR FS:[00000000],ESP

  注意一系列的 PUSH 指令。它们在堆栈上创建一个数据结构(有点像一个不可见的局部变量)。该指令在偏移 0x_40100F 处从线程信息块中取出结构化异常处理链的头,然后将之存储到 EAX——指令的 FS:[00000000] 部分这正是来自于此。此时代码将当前结构化异常处理链的头压入堆栈。至此结束在堆栈上创建局部数据结构的过程。最后一条指令:

 MOV DWORD PTR FS:[00000000],ESP

  将结构化异常处理链头指向新创建的数据结构。

  此处关键是 Win32 编译器暗中知道 TIB 的信息并产生访问它的代码。因为编译器无法知道代码运行在哪个 Win32 系统上,你可以安全地假设任何引用 FS 段的编译器产生的代码使用的都是 TIB 在 Win32 平台上的公用域。

  TIB 中的公用域

  你刚看了一个 TIB 结构的例子,其中就有在所有 Win32 平台上公用域。在这一部分,我会列出所有的公用域以及简短说明。至于在Win32 平台之间有差别的那些域以后来讲。

  定义 TIB 结构域的头文件零零散散有几个。不幸的是,这些文件对 TIB 的定义相互并不一致和完整。在 Windows NT DDK 中,你会发现 NTDDK.H 中定义为 NT_TIB 结构。在 Windows NT 3.51 的补丁包 SP3 更新文件中,新的 WINNT.H 文件也定义了一个 NT_TIB 结构。此外,Windows 95 的开发人员在线发布的.H 文件内容中是 TIB。在我的描述中,我尝试使用广泛认可的名称。你会在 TIB.H 文件中看到这些名称,这个文件将随本文例子程序 SHOWTIB 一起提供:

TIB.H

//===========================================================
// File: TIB.H
// Author: Matt Pietrek
// From: Microsoft Systems Journal "Under the Hood", May 1996
//===========================================================
#pragma pack(1)

typedef struct _EXCEPTION_REGISTRATION_RECORD
{
  struct _EXCEPTION_REGISTRATION_RECORD * pNext;
  FARPROC                 pfnHandler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

typedef struct _TIB
{
PEXCEPTION_REGISTRATION_RECORD pvExcept; // 00h Head of exception record list
PVOID  pvStackUserTop;   // 04h Top of user stack
PVOID  pvStackUserBase;  // 08h Base of user stack

union            // 0Ch (NT/Win95 differences)
{
  struct // Win95 fields
  {
    WORD  pvTDB;     // 0Ch TDB
    WORD  pvThunkSS;   // 0Eh SS selector used for thunking to 16 bits
    DWORD  unknown1;   // 10h
  } WIN95;

  struct // WinNT fields
  {
    PVOID SubSystemTib;   // 0Ch
    ULONG FiberData;    // 10h
  } WINNT;
} TIB_UNION1;

PVOID  pvArbitrary;    // 14h Available for application use
struct _tib *ptibSelf;   // 18h Linear address of TIB structure

union            // 1Ch (NT/Win95 differences)
{
  struct // Win95 fields
  {
    WORD  TIBFlags;      // 1Ch
    WORD  Win16MutexCount;  // 1Eh
    DWORD  DebugContext;    // 20h
    DWORD  pCurrentPriority;  // 24h
    DWORD  pvQueue;      // 28h Message Queue selector
  } WIN95;

  struct // WinNT fields
  {
    DWORD unknown1;       // 1Ch
    DWORD processID;      // 20h
    DWORD threadID;       // 24h
    DWORD unknown2;       // 28h
  } WINNT;
} TIB_UNION2;

PVOID* pvTLSArray;     // 2Ch Thread Local Storage array

union            // 30h (NT/Win95 differences)
{
  struct // Win95 fields
  {
    PVOID* pProcess;   // 30h Pointer to owning process database
  } WIN95;
} TIB_UNION3;
  
} TIB, *PTIB;
#pragma pack()
SHOWTIB.CPP

//==========================================================================
// File: SHOWTIB.CPP
// Author: Matt Pietrek
// To Build:
// CL /MT SHOWTIB.CPP USER32.LIB (Visual C )
// BCC32 -tWM SHOWTIB.CPP (Borland C , TASM32 required)
//==========================================================================
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#pragma hdrstop
#include "tib.h"

#define SHOWTIB_MAX_THREADS 64

CRITICAL_SECTION gDisplayTIB_CritSect;

void DisplayTIB( PSTR pszThreadName )
{
  PTIB pTIB;
  WORD fsSel;

  EnterCriticalSection( &gDisplayTIB_CritSect );

  __asm
  {
    mov   EAX, FS:[18h]
    mov   [pTIB], EAX
    mov   [fsSel], FS
  }
    
  printf( "Contents of thread %sn", pszThreadName );
  
  printf( " TIB X (Address: X)n", fsSel, pTIB );
  printf( " SEH chain: Xn", pTIB->pvExcept );
  printf( " Stack top: Xn", pTIB->pvStackUserTop );
  printf( " Stack base: Xn", pTIB->pvStackUserBase );
  printf( " pvArbitray: Xn", pTIB->pvArbitrary );
  printf( " TLS array *: Xn", pTIB->pvTLSArray );

  printf( " ----OS Specific fields----n" );
  if ( 0xC0000000 == (GetVersion() & 0xC0000000) )  // Is this Win95 ?
  {
    printf( " TDB: Xn", pTIB->TIB_UNION1.WIN95.pvTDB );
    printf( " Thunk SS: Xn", pTIB->TIB_UNION1.WIN95.pvThunkSS );
    printf( " TIB flags: Xn", pTIB->TIB_UNION2.WIN95.TIBFlags );
    printf( " Win16Mutex count: Xn",          pTI->TIB_UNION2.WIN95.Win16MutexCount );
    printf( " DebugContext: Xn", pTIB->TIB_UNION2.WIN95.DebugContext );
    printf( " Current Priority *: X (%u)n",
          pTIB->TIB_UNION2.WIN95.pCurrentPriority,
          *(PDWORD)(pTIB->TIB_UNION2.WIN95.pCurrentPriority) );
    printf( " Queue: Xn", pTIB->TIB_UNION2.WIN95.pvQueue );
    printf( " Process *: Xn", pTIB->TIB_UNION3.WIN95.pProcess );
  }
  else if ( 0 == (GetVersion() & 0xC0000000) )  // Is this WinNT ?
  {
    printf(" SubSystem TIB: Xn", pTIB->TIB_UNION1.WINNT.SubSystemTib);
    printf(" FiberData: Xn", pTIB->TIB_UNION1.WINNT.FiberData );
    printf(" unknown1: Xn", pTIB->TIB_UNION2.WINNT.unknown1);
    printf(" process ID: Xn", pTIB->TIB_UNION2.WINNT.processID);
    printf(" thread ID: Xn", pTIB->TIB_UNION2.WINNT.threadID);
    printf(" unknown2: Xn", pTIB->TIB_UNION2.WINNT.unknown2);
  }
  else
  {
    printf(" Unsupported Win32 implementationn" );
  }
  
  printf( "n" );

  LeaveCriticalSection( &gDisplayTIB_CritSect );
}

void MyThreadFunction( void * threadParam )
{
  char szThreadName[128];

  wsprintf( szThreadName, "%u", threadParam );  // Give the thread a name

  // If multiple threads are specified, give'em different priorities
  if ( (DWORD)threadParam & 1 )
    SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_HIGHEST );

  DisplayTIB( szThreadName );   // Display the thread's TIB

  // Let other threads execute while this thread is still alive. The idea
  // here is to try and prevent memory region and selector reuse.
  Sleep( 1000 );
}

int main( int argc, char *argv[] )
{
  if ( argc < 2 )
  {
    printf( "Syntax: SHOWTIB [# of threads]n" );
    return 1;
  }

  InitializeCriticalSection( &gDisplayTIB_CritSect );
  
  unsigned cThreads = atoi( argv[1] );

  if ( (cThreads < 1) || (cThreads > SHOWTIB_MAX_THREADS) )
  {
    printf( "thread count must be > 1 and < %un", SHOWTIB_MAX_THREADS );
  }
  else
  {
    // Allocate an array to hold the thread handles
    HANDLE threadHandles[ SHOWTIB_MAX_THREADS ];

    // Create the specified number of threads
    for ( unsigned i = 0; i < cThreads; i  )
      threadHandles[i] = (HANDLE)
        _beginthread(MyThreadFunction,0,(PVOID)i);

    // Wait for all the threads to finish before we exit the program
    WaitForMultipleObjects( cThreads, threadHandles, TRUE, INFINITE );

    // We don't need the thread handles anymore. Close'em!
    for ( i = 0; i < cThreads; i  )
      CloseHandle( threadHandles[i] );
  }
  
  DeleteCriticalSection( &gDisplayTIB_CritSect );

  return 0;
} 

  00h DWORD pvExcept 域包含一个指针指向线程的结构化异常处理链头。这个链是一个 EXCEPTION_REGISTRATION_RECORD 结构链表(不幸的是这个结构没有在任何正式的 .H 文件中定义)。有关结构化异常处理链的更多信息,请参考我的拙作《Windwos 95 系统编程奥秘》。

  04h DWORD pvStackUserTop 域是线程堆栈顶层地址的线性地址。或者说,线程的堆栈指针值将不会大于等于这个域的值。

  08h DWORD pvStackUserBase 域是线程用户模式堆栈最低约束页面的线性地址,由于线程使用堆栈中连续的地位地址,那些页面将会被提交,相应地会更新此域。

  14h DWORD pvArbitrary 域理论上说,应用程序可以使用此域,它有点像额外的线程局部存储槽,但我从来没有看到过有程序使用过它。

  18h DWORD ptibSelf 用来保存 TIB 的线性地址。或者说,TIB 块包括一个自身的指针。为什么要这么做呢?如果要扩展 TIB 结构域的使用,那么就可以使得32位代码用常规指针读写 TIB,而不是用段寄存器操作(例如使用 FS:[xxxxxxxx])。SHOWTIB 程序就使用了这个域。

  2Ch DWORD pvTLSArray 域保存线程局部存储(TLS)槽的指针。例如,如果你的 TLS 索引值是 4,你用这个指针上加 10h(4 * sizeof(DWORD)),并直接获取 TLS 值。知道了这个域指向线程的 TLS 槽,你便可以很容易编写自己的 TlsSetValue 和 TlsGetValue。如果在代码中使用 __declspec(thread) 变量,察看一下编译器产生的 ASM 代码。你就会发现它使用了该结构域。

  TLS 槽的位置在不用的 Windows 平台上差别很大。Windows NT 中,该域保存一个空指针,直到线程第一次使用 TLS 槽为止,然后系统在默认的进程堆外为 TLS 槽分配内存。(在 Windows NT 中,某个 HeapAlloc 块的缓冲溢出能导致 TLS 数据紊乱 )。在 Windows 95 中,该域总是指向 TLS 槽,作为 Ring3 线程数据库的一部分。

  Windows NT TIB 域

  TIB 中某些数据当运行在不同的 Windows 平台上其意义是不同的。OS/2 子系统-Windows NT 这部分数据中支持运行 OS/2.1.X 应用。而对于常规的 Win32 应用,这个域的值总是零。

  10h DWORD FiberData 域根据线程运行在不同的 Windows NT 版本其意义不同。在 Windows NT 3.51 SP3 SDK 更新中,WINNT.H 描述将此域描述为指向光线数据,关于光线在 HLP 帮助文件中被描述为手动调度的轻量级线程。在此服务包更新之前,这个域叫 “Version”。我推测意思是线程期望运行的系统版本,但是我无法这个域中该值的意义。

  20h DWORD processID 域保存线程的进程 ID。Windows NT  3.51 中的 GetCurrentProcessId 函数返回的任何值都保存其中。

  24h DWORD threadID 域是线程 ID。保存 Windows NT  3.51 中的 GetCurrentThreadId 函数返回值。

  Windows NT TIB 段指向的位置实际上大大了超出了在此描述的这个域的范围。而我前面提到的这个域位置是在最前面的 34h 字节之内(Windows 95 TIB 的尺寸)

  Windows 95 TIB

  与 Windows NT TIB 相比,Windows 95 TIB 包含相当多令人感兴趣的信息,在这一部分作详细描述。

  0CH WORD pvTDB 是任务数据库选择器,这里的任务是与线程关联的。任务数据库是一个段,它从 16位的全局堆中分配,句柄就是 HTASK。Windows 95 中每个进程(即使在 Win32 进程中)都具备一个任务数据库。

  0EH WORD pvThunkSS 是 Windows 95 用于保存 16位堆选择器的,当某个线程从 32位转换到16位时要用到它。

  1CH WORD TIBFlags 被用于保存不同的位标志。也就是 TIB_WIN32(即 1)。如果置在该值的低位,表示是32位线程,否则表示线程来自一个16位进程。

  1Eh WORD Win16MutexCount 与 Win16Mutex 的线程所有者有关,它是一个全局临界块,在16位代码中一次只允许一个线程。通常,这个域的值是-1,表示线程不拥有 Win16Mutex。当线程进入和离开要转换代码,这个值是也因此相应增加和减少。

  20h DWORD DebugContext 域通常包含的值是零。但当你调试线程的进程时,该域指向一个包含寄存器值结构,类似于但不等同于在 WINNT.H 中定义的 CONTEXT 结构。

  24h DWORD pCurrentPriority 指向一个 DWORD,它包含线程的调度优先级。值介于0(最小)到31(最大)之间。此域指向的 DWORD 在线性内存中在 3GB 以上的位置,它放在 VxD 领地。这也就是说线程的调度是由 ring 0 Virtual Machine Manager (VMM) 进行的。对于常规优先级的线程,优先级 DWORD 的值是9。

  28h DWORD pvQueue 包含赋值给线程的消息队列选择器。即窗口消息通过消息对列到达相应的窗口。在 Windows 95 中,每个线程都具备自己的消息队列,但它初始状态不创建消息队列。因此,这个域的值可能是零。

  30h PVOID* pProcess 是代表拥有线程的进程的进程数据库线性地址,但是它不能等同于进程句柄或进程 ID。

  关于 TIBs 的一些注释

  写本专栏对 TIB 进行实验的时候,我得一些重要信息值得与大家分享。首先,在 Windows 95 中,在每个任务数据库段的 52h 的偏移处,你会发现进程主线程的 TIB 选择器。在任务数据库段的 54h 的偏移处,你会找到 TIB 的线性地址。这个地址特别有趣,其中的任务数据库是被 Windows 95 的 16位组件使用的。似乎16位组件有时候会访问 TIB 中线程专用的数据。

  我还注意到 FS 寄存器在 Windows NT 和 Windows 95 之间的使用差别,在 Windows NT 中,对于每个线程的 TIB来说,FS 寄存器总是一样的。这也就是说用于 FS 选择器的线性地址在任何线程发生转换的时候都得改变。相反, Windows 95 为每个 TIB 使用不同的选择器。因此,对每个线程而言,Windows 95 TIB 选择器的线性地址不会变。对于系统资源来说,你想哪种方法更优。

  SHOWTIB 程序

  为了了解 TIB 在代码中的实现,我写了 SHOWTIB 程序。SHOWTIB 是一个简单的命令行程序,有两个目的,第一个是创建多个线程(有你在命令行指定实际的线性数,例如,“SHOWTIB 5”就是让 SHOWTIB 新创建五个线程并显示这些线程的 TIBs)。第二个目的是所有线程运行起来之后显示每个线程的 TIB 结构数据。我只显示由主线程创建的线程 TIB 数据,不是主线程自己的 TIB 数据。

  在显示每个线程的 TIB 时,SHOWTIB 首先显示所有 Win32 操作系统公共的数据域,然后确定当前是运行在 Windows NT 系统中还是 Windows 95 系统中。根据平台系统信息 SHOWTIB 显示相关平台的 TIB 数据域。为了让 TIB 信息显示得连贯和一致,DisplayTIB 函数使用临界区保证代码正常工作。

  SHOWTIB.CPP 中有两个有趣的地方,DisplayTIB 函数的开始处,代码使用了几行内联汇编以便在 TIB 18h 偏移处获取数据域并存储到一个指针。偏移 18h 是 TIB 的线性地址。我之所以这么做是我能用常规指针访问其余的 TIB 数据域。还有其它的内联汇编方法和 FS 段改写方法来获取所有数据值。Win32 编译器不会只有一种途径来轻松读取数据段(DS 寄存器)之外的任何段数据的。

  第二个有趣的地方是代码 main 函数最后,在创建了所有线程并将对应的线程句柄存储到数组之后,我调用了 WaitForMultipleObjects 函数,传入线程句柄数组。如果我不这样做,主线程可能会在工作线程终止之前从 main 例程返回并调用退出函数 exit。这样将导致显示的 TIBs 数据不完整。

  虽然 Windows NT 和 Windows 95 的内核差别很大,但 TIBs 是你能依赖的为数不多的几个共用的数据结构。这不是巧合,因为线程概念是两个操作系统重要的组成部分,所以自然就会某些提供线程专用信息的公共方法。除了 .H 文件之外,TIB 没有在任何正式的文档中描述。但是,它是所有 Win32 规范的重要组成部分,Win32 实现必须遵循它。

Win32 系统线程信息块(TIB)浅析本文由 VCKBASE MTT 翻译