⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 漫谈兼容内核之二十三关于tls.txt

📁 漫谈系统内核内幕 收集得很辛苦 呵呵 大家快下在吧
💻 TXT
📖 第 1 页 / 共 3 页
字号:

  if (LdrpTlsCount > 0)
  {
    LdrpTlsArray = RtlAllocateHeap(RtlGetProcessHeap(),0,
                                      LdrpTlsCount * sizeof(TLS_DATA));
    if (LdrpTlsArray == NULL)
    {
           return STATUS_NO_MEMORY;
    }
    ModuleListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;
    Entry = ModuleListHead->Flink;
    while (Entry != ModuleListHead)
    {
      Module =
              CONTAINING_RECORD(Entry, LDR_MODULE, InLoadOrderModuleList);
      if (Module->LoadCount == -1 && Module->TlsIndex >= 0)
      {
        TlsDirectory = (PIMAGE_TLS_DIRECTORY)
                      RtlImageDirectoryEntryToData(Module->BaseAddress, TRUE,
                                    IMAGE_DIRECTORY_ENTRY_TLS, NULL);
        assert(Module->TlsIndex < LdrpTlsCount);
        TlsData = &LdrpTlsArray[Module->TlsIndex];
        TlsData->StartAddressOfRawData = (PVOID)TlsDirectory->StartAddressOfRawData;
        TlsData->TlsDataSize = TlsDirectory->EndAddressOfRawData -
                                             TlsDirectory->StartAddressOfRawData;
        TlsData->TlsZeroSize = TlsDirectory->SizeOfZeroFill;
        if (TlsDirectory->AddressOfCallBacks)
          TlsData->TlsAddressOfCallBacks = *TlsDirectory->AddressOfCallBacks;
        else
          TlsData->TlsAddressOfCallBacks = NULL;
        TlsData->Module = Module;
        /* FIXME: Is this region allways writable ? */
        *(PULONG)TlsDirectory->AddressOfIndex = Module->TlsIndex;
        CHECKPOINT1;
      }
      Entry = Entry->Flink;
    }
  }
  return STATUS_SUCCESS;
}

    可想而知,对于把所有.tls段合并在一起的静态TLS,每个线程都需要有个副本;但是线程是随时都可以创建的,所以必须把来自各模块映像.tls段的原始映像合并保存起来。然而各个.tls段既然分别存在于已被装入的模块映像中,而这些.tls段又不属于任何具体的线程,在运行不会被修改,就无需再单独存储一份合并以后的版本,但是需要有个合并的目录,以便需要时可以方便地找到这些原始的版本。这正是LdrpInitializeTlsForProccess()要做的。
    所以,为实现静态TLS,每个进程的用户空间需要一个类似于目录的指针数组LdrpTlsArray[],其中的每个指针都指向一个TLS_DATA数据结构,而每个TLS_DATA数据结构则代表着一个模块的.tls段。

typedef struct _TLS_DATA
{
   PVOID StartAddressOfRawData;
   DWORD TlsDataSize;
   DWORD TlsZeroSize;
   PIMAGE_TLS_CALLBACK TlsAddressOfCallBacks;
   PLDR_MODULE Module;
} TLS_DATA, *PTLS_DATA;

    这个数据结构本身并不提供实际的TLS,其指针StartAddressOfRawData才指向实际的存储空间、即各个模块的.tls段。每个.tls段的开头可以有一些非0初值的TLS变量,TlsDataSize说明了这一部分的大小。其余则都是初值为0的TLS变量,字段TlsZeroSize说明了这一部分的大小。此外,含有.tls段的模块可能同时提供一些用于初始化或需要在特定条件下执行的“回调”函数,指针TlsAddressOfCallBacks指向一个用来提供有关函数指针的IMAGE_TLS_CALLBACK数据结构。至于指针Module,则只是说明这个.tls段来自哪一个模块,同时也便于从具体模块的数据结构中获取更多、更详尽的有关信息。例如,其中的FullDllName就提供了该模块的映像文件名。
    上面的程序中根据.tls段的个数LdrpTlsCount分配了数组LdrpTlsArray[]所需的空间。注意LdrpTlsCount只反映了静态装入的.tls段的个数,而动态装入的模块到底会有多少是无法预测的。
    然后扫描本进程的“已装入模块队列”、即PEB下面的InLoadOrderModuleList,为每个模块的.tls段(如果有的话)建立LdrpTlsArray[]中的目录项。注意此时所有静态装入的模块都已经在这个队列中,所以这是一次完全的扫描。另一方面,凡是带有.tls段、或者说.tls段的大小非0的模块,前面都已经为其分配了TLS索引号TlsIndex,所以在目录中的位置也已经确定。
    从程序中可以看出,具体的原始TLS映像仍在各模块的内存映像中,但是现在有了一个统一的目录LdrpTlsArray[]。
    再回到LdrPEStartup()的代码,下面的LdrpAttachProcess()主要是以常数DLL_PROCESS_ATTACH为参数调用各个DLL模块的DllMain(),与静态TLS的实现没有什么关系。而LdrpTlsCallback()则是以DLL_PROCESS_ATTACH为参数调用各模块的TLS回调函数(如果有的话),我们在这里就不深入下去了。
    这些操作都是进程一级的,所以只是进程中的第一个线程才加以执行,以后创建的线程就不再执行这些操作了。
    从LdrPEStartup()返回到__true_LdrInitializeThunk(),在完成了所有别的操作以后,对于静态TLS而言,就是线程一级的操作了,这是每个线程都要执行的,目的在于为当前线程复制一个本地的静态TLS副本。这是在LdrpAttachThread()里面完成的:

[__true_LdrInitializeThunk() > LdrpAttachThread()]

NTSTATUS
LdrpAttachThread (VOID)
{
  PLIST_ENTRY ModuleListHead;
  PLIST_ENTRY Entry;
  PLDR_MODULE Module;
  NTSTATUS Status;

  DPRINT("LdrpAttachThread() called for %wZ\n",
         &ExeModule->BaseDllName);

  RtlEnterCriticalSection (NtCurrentPeb()->LoaderLock);

  Status = LdrpInitializeTlsForThread();

  if (NT_SUCCESS(Status))
    {
      ModuleListHead = &NtCurrentPeb()->Ldr->InInitializationOrderModuleList;
      Entry = ModuleListHead->Flink;

      while (Entry != ModuleListHead)
        {
          Module = CONTAINING_RECORD
(Entry, LDR_MODULE, InInitializationOrderModuleList);
          if (Module->Flags & PROCESS_ATTACH_CALLED &&
              !(Module->Flags & DONT_CALL_FOR_THREAD) &&
              !(Module->Flags & UNLOAD_IN_PROGRESS))
            {
              TRACE_LDR("%wZ - Calling entry point at %x for thread attaching\n",
                        &Module->BaseDllName, Module->EntryPoint);
              LdrpCallDllEntry(Module, DLL_THREAD_ATTACH, NULL);
            }
          Entry = Entry->Flink;
        }

      Entry = NtCurrentPeb()->Ldr->InLoadOrderModuleList.Flink;
      Module = CONTAINING_RECORD(Entry, LDR_MODULE, InLoadOrderModuleList);
      LdrpTlsCallback(Module, DLL_THREAD_ATTACH);
    }

  RtlLeaveCriticalSection (NtCurrentPeb()->LoaderLock);

  DPRINT("LdrpAttachThread() done\n");

  return Status;
}

    显然,这里LdrpInitializeTlsForThread()的作用就是为当前线程复制一个“本地”的TLS副本:

[__true_LdrInitializeThunk() > LdrpAttachThread() > LdrpInitializeTlsForThread()]

static NTSTATUS
LdrpInitializeTlsForThread(VOID)
{
   PVOID* TlsPointers;
   PTLS_DATA TlsInfo;
   PVOID TlsData;
   ULONG i;

   DPRINT("LdrpInitializeTlsForThread() called for %wZ\n", &ExeModule->BaseDllName);

   if (LdrpTlsCount > 0)
   {
       TlsPointers = RtlAllocateHeap(RtlGetProcessHeap(),0,
                                     LdrpTlsCount * sizeof(PVOID) + LdrpTlsSize);
       if (TlsPointers == NULL)
       {
           DPRINT1("failed to allocate thread tls data\n");
           return STATUS_NO_MEMORY;
       }

       TlsData = (PVOID)TlsPointers + LdrpTlsCount * sizeof(PVOID);
       NtCurrentTeb()->ThreadLocalStoragePointer = TlsPointers;

       TlsInfo = LdrpTlsArray;
       for (i = 0; i < LdrpTlsCount; i++, TlsInfo++)
       {
           TRACE_LDR("Initialize tls data for %wZ\n", &TlsInfo->Module->BaseDllName);
           TlsPointers[i] = TlsData;
           if (TlsInfo->TlsDataSize)
           {
               memcpy(TlsData, TlsInfo->StartAddressOfRawData, TlsInfo->TlsDataSize);
               TlsData += TlsInfo->TlsDataSize;
           }
           if (TlsInfo->TlsZeroSize)
           {
               memset(TlsData, 0, TlsInfo->TlsZeroSize);
               TlsData += TlsInfo->TlsZeroSize;
           }
       }
   }
   DPRINT("LdrpInitializeTlsForThread() done\n");
   return STATUS_SUCCESS;
}

    注意这里分配的空间大小是LdrpTlsCount * sizeof(PVOID) + LdrpTlsSize,这是一个大小为LdrpTlsCount的指针数组加上合并以后的整个静态TLS的大小。然后就根据静态TLS目录把所有.tls段的原始副本收集汇总并复制到所分配的缓冲区中。这样,就为一个线程构建了一个合并的静态TLS副本。至于指针数组TlsPointers[ ]中的每个指针,则各自指向相应.tls段的数据在这个副本中的起点。
    还要注意,指针TlsPointers填写在当前线程TEB的字段ThreadLocalStoragePointer中。所以,找到了一个线程的TEB,就可以根据这个指针找到其静态TLS副本。
    由于每个线程在启动时都要执行__true_LdrInitializeThunk(),因而都要执行这个函数,所以就都会有各自的静态TLS副本。
    回到LdrpAttachThread(),后面以DLL_THREAD_ATTACH为参数对各个模块调用LdrpCallDllEntry()和LdrpCallDllEntry(),这里就不看了。

    那么,在应用程序中怎样访问静态TLS变量呢?如前所述,这需要编译工具和库程序的配合。可以想像,库程序应提供一个类似于__errno_location()那样的函数,但是以目标变量所在模块的索引号和目标变量在其.tls段内部的位移为参数。这样,只要先找到当前线程的TEB,就可以找到其静态TLS副本。然后,以具体TLS变量所在模块的索引号为下标可以通过其指针数组找到目标所在的.tls段副本,再用目标TLS变量在.tls段内的位移就可以找到其地址。
    可见静态TLS其实一点不比动态TLS简单,反倒复杂得多。
    早期的程序员也许觉得使用动态TLS需要在程序中通过TlsSetValue()、TlsGetValue()一类的函数才能访问TLS变量,不如使用静态TLS那样方便;但是现在大家都已经习惯了面向对象的程序设计,而面向对象的程序设计本来就得通过对象所提供的“方法(method)”进行访问,因此也就不感到不方便了。
    作为一个例子,我们再看一下errno。本来,W32 API的界面并不需要使用errno,也不存在为此而需要使用TLS的问题;但是微软为了实现对POSIX子系统的支持而只好也来实现一个标准C程序库的界面,因此就也有了如何实现errno的问题,下面的代码仍取自ReactOS:

#define errno (*__PdxGetThreadErrNum())

    显然,__PdxGetThreadErrNum()相当于__errno_location()。再看它是如何实现的:

int * __PdxGetThreadErrNum(void)
{
return &(((__PPDX_TDATA)
(NtCurrentTeb()->TlsSlots[__PdxGetProcessData()->TlsIndex]) )->ErrNum);
}

    可见,这实际上是在用动态TLS来实现静态TLS,因为TEB中的TlsSlots[]是用于动态TLS的。

    最后,表面上TLS的实现都是用户空间的事,似乎与内核无关;但是实际上内核为用户空间TLS的实现提供了基础。这是因为,无论是与TEB挂勾,还是与用户空间堆栈挂勾,还是使用段寄存器寻址,实际上都是建立在分段存储的基础上;但实际上正是内核在维持段描述表GDT或LDT中段描述项的切换,并使段寄存器FS或GS在CPU进入用户空间时持有相应的选择项。
    此外,在系统调用NtSetInformationThread()中有两个选项是与TLS有关的,一个是ThreadZeroTlsCell,另一个是ThreadSetTlsArrayAddress。前者的作用据说是将一个(大概是静态)TLS变量清0。后者则是为“TLS数组”、应该是当事线程的静态TLS副本吧、指定一个地址。为此,在KTHREAD数据结构中还设了一个字段TlsArray,但是在ReactOS的代码中未见使用。

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -