对TLS底层实现的详解

我在之前的博客里并未提及过Tls的底层初始化,现在好好来谈谈。
首先我们要知道Tls的初始化是在入口之前。先从一开始的LdrInitializeThunk说起,在调用LdrPEStartup时,其在修复完导入表后,调用了一次LdrpInitializeTlsForProccess,我们看看他是怎样处理的。

static NTSTATUS
LdrpInitializeTlsForProccess(VOID)
{
   PLIST_ENTRY ModuleListHead;
   PLIST_ENTRY Entry;
   PLDR_DATA_TABLE_ENTRY Module;
   PIMAGE_TLS_DIRECTORY TlsDirectory;
   PTLS_DATA TlsData;
   ULONG Size;
   DPRINT("LdrpInitializeTlsForProccess() called for %wZn", &ExeModule->BaseDllName);
   if (LdrpTlsCount > 0)
     {
       LdrpTlsArray = RtlAllocateHeap(RtlGetProcessHeap(),
                                      0,
                                      LdrpTlsCount * sizeof(TLS_DATA));//分配了一个容纳所有Tls变量的空间,基于线程
       if (LdrpTlsArray == NULL)
         {
           DPRINT1("Failed to allocate global tls data\n");
           return STATUS_NO_MEMORY;
         }
       ModuleListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;
       Entry = ModuleListHead->Flink;
       while (Entry != ModuleListHead)//以装载顺序遍历
         {
           Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
           if (Module->LoadCount == LDRP_PROCESS_CREATION_TIME &&
               Module->TlsIndex != 0xFFFF)
             {
               TlsDirectory = (PIMAGE_TLS_DIRECTORY)//获得TLS目录表
                                 RtlImageDirectoryEntryToData(Module->DllBase,
                                                              TRUE,
                                                              IMAGE_DIRECTORY_ENTRY_TLS,
                                                              &Size);
               ASSERT(Module->TlsIndex < LdrpTlsCount);
               TlsData = &LdrpTlsArray[Module->TlsIndex];//TlsData存放的是记录着该模块Tls信息的指针,其index在TlsIndex里
               TlsData->StartAddressOfRawData = (PVOID)TlsDirectory->StartAddressOfRawData;//对该模块的信息进行填充
               TlsData->TlsDataSize = TlsDirectory->EndAddressOfRawData - TlsDirectory->StartAddressOfRawData;
               TlsData->TlsZeroSize = TlsDirectory->SizeOfZeroFill;
               if (TlsDirectory->AddressOfCallBacks)//如果存在TLS回调函数
                 TlsData->TlsAddressOfCallBacks = (PIMAGE_TLS_CALLBACK *)TlsDirectory->AddressOfCallBacks;//放入到其中,TlsAddressOfCallBacks数组是函数指针数组
               else
                 TlsData->TlsAddressOfCallBacks = NULL;
               TlsData->Module = Module;
#if 0
               DbgPrint("TLS directory for %wZn", &Module->BaseDllName);
               DbgPrint("StartAddressOfRawData: %xn", TlsDirectory->StartAddressOfRawData);
               DbgPrint("EndAddressOfRawData:   %xn", TlsDirectory->EndAddressOfRawData);
               DbgPrint("SizeOfRawData:         %dn", TlsDirectory->EndAddressOfRawData - TlsDirectory->StartAddressOfRawData);
               DbgPrint("AddressOfIndex:        %xn", TlsDirectory->AddressOfIndex);
               DbgPrint("AddressOfCallBacks:    %xn", TlsDirectory->AddressOfCallBacks);
               DbgPrint("SizeOfZeroFill:        %dn", TlsDirectory->SizeOfZeroFill);
               DbgPrint("Characteristics:       %xn", TlsDirectory->Characteristics);
#endif
               /*
                * FIXME:
                *   Is this region allways writable ?
                */
               *(PULONG)TlsDirectory->AddressOfIndex = Module->TlsIndex;
             }
           Entry = Entry->Flink;
        }
    }
  DPRINT("LdrpInitializeTlsForProccess() donen");
  return STATUS_SUCCESS;
}

/*
以加载模块的顺序,读取TLS目录,将信息填入到LdrpTlsArray[Module->TlsIndex]指定的索引结构中,其中Module中的索引值起到决定作用,之后内核就可以使用LdrpTlsArray和index来使用TLS
*/

以加载模块的顺序,读取TLS目录,将信息填入到LdrpTlsArray[Module->TlsIndex]指定的索引结构中,其中Module中的索引值起到决定作用,之后内核就可以使用LdrpTlsArray和index来使用TLS。这里就是为内核操作TLS提供了保障,TLS信息被收录到LdrpTlsArray中。

再来看下LdrpAttachThread(),这个是在LdrInitializeThunk中最后被使用的。

NTSTATUS
LdrpAttachThread (VOID)
{
  PLIST_ENTRY ModuleListHead;
  PLIST_ENTRY Entry;
  PLDR_DATA_TABLE_ENTRY Module;
  NTSTATUS Status;
  DPRINT("LdrpAttachThread() called for %wZn",
         &ExeModule->BaseDllName);
  RtlEnterCriticalSection (NtCurrentPeb()->LoaderLock);//加锁
  Status = LdrpInitializeTlsForThread();//下发到LdrpInitializeTlsForThread
  if (NT_SUCCESS(Status))
    {
      ModuleListHead = &NtCurrentPeb()->Ldr->InInitializationOrderModuleList;
      Entry = ModuleListHead->Flink;
      while (Entry != ModuleListHead)//以初始化顺序遍历模块
        {
          Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InInitializationOrderModuleList);
          if (Module->Flags & LDRP_PROCESS_ATTACH_CALLED &&
              !(Module->Flags & LDRP_DONT_CALL_FOR_THREADS) &&
              !(Module->Flags & LDRP_UNLOAD_IN_PROGRESS))
            {//若DLL_PROCESS_ATTACH被调用了,现在轮到DLL_THREAD_ATTACH
              TRACE_LDR("%wZ - Calling entry point at %x for thread attachingn",
                        &Module->BaseDllName, Module->EntryPoint);
              LdrpCallDllEntry(Module, DLL_THREAD_ATTACH, NULL);
            }
          Entry = Entry->Flink;
        }
      Entry = NtCurrentPeb()->Ldr->InLoadOrderModuleList.Flink;
      Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
      LdrpTlsCallback(Module, DLL_THREAD_ATTACH);
    }
  RtlLeaveCriticalSection (NtCurrentPeb()->LoaderLock);
  DPRINT("LdrpAttachThread() donen");
  return Status;
}
/*
LdrpInitializeTlsForThread
LdrpCallDllEntry
LdrpTlsCallback
*/

LdrpAttachThread->LdrpInitializeTlsForThread->LdrpCallDllEntry->LdrpTlsCallback
它将初始化工作交给LdrpInitializeTlsForThread,然后发出dwReason == DLL_THREAD_ATTACH,调用线程的TLS初始化,再来看下LdrpInitializeTlsForThread

static NTSTATUS
LdrpInitializeTlsForThread(VOID)
{
   PVOID* TlsPointers;
   PTLS_DATA TlsInfo;
   PVOID TlsData;
   ULONG i;
   PTEB Teb = NtCurrentTeb();
   DPRINT("LdrpInitializeTlsForThread() called for %wZn", &ExeModule->BaseDllName);
   Teb->StaticUnicodeString.Length = 0;
   Teb->StaticUnicodeString.MaximumLength = sizeof(Teb->StaticUnicodeBuffer);
   Teb->StaticUnicodeString.Buffer = Teb->StaticUnicodeBuffer;
   if (LdrpTlsCount > 0)//若存在线程局部变量
     {
       TlsPointers = RtlAllocateHeap(RtlGetProcessHeap(),
                                     0,
                                     LdrpTlsCount * sizeof(PVOID) + LdrpTlsSize);
       if (TlsPointers == NULL)
         {
           DPRINT1("failed to allocate thread tls datan");
           return STATUS_NO_MEMORY;
         }
       TlsData = (PVOID)((ULONG_PTR)TlsPointers + LdrpTlsCount * sizeof(PVOID));//移到离边界还有LdrpTlsSize处
       Teb->ThreadLocalStoragePointer = TlsPointers;
       TlsInfo = LdrpTlsArray;//将之前的array赋值给TlsInfo array存放的是指针数组,指向的是每一个模块的目录信息
       for (i = 0; i < LdrpTlsCount; i++, TlsInfo++)//将对应的每一个模块的TLS目录指定的StartAddressOfRawData开始处的大小为TlsDataSize的数据拷贝到ThreadLocalStoragePointer处
         {
           TRACE_LDR("Initialize tls data for %wZn", &TlsInfo->Module->BaseDllName);
           TlsPointers[i] = TlsData;
           if (TlsInfo->TlsDataSize)
             {
               memcpy(TlsData, TlsInfo->StartAddressOfRawData, TlsInfo->TlsDataSize);
               TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsDataSize);
             }
           if (TlsInfo->TlsZeroSize)
             {
               memset(TlsData, 0, TlsInfo->TlsZeroSize);
               TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsZeroSize);
             }
         }
     }
   DPRINT("LdrpInitializeTlsForThread() donen");
   return STATUS_SUCCESS;
}

真正装载ThreadLocalStoragePointer的是由LdrpInitializeTlsForThread来执行的,可以认为LdrpInitializeTlsForProccess是对LdrpInitializeTlsForThread的初始化,它只是对所有模块TLS目录表的提取。而LdrpInitializeTlsForThread则是对LdrpTlsArray的内容的提取。所以之后只需要NtCurrentTeb()->ThreadLocalStoragePointer来完成Tls变量的存取工作就可以了。

紧接着看下LdrpInitializeTlsForThread初始化ThreadLocalStoragePointer之后LdrpCallDllEntry又干了啥吧

static BOOLEAN LdrpCallDllEntry(PLDR_DATA_TABLE_ENTRY Module, DWORD dwReason, PVOID lpReserved)
{
   if (!(Module->Flags & LDRP_IMAGE_DLL) ||//如果不是DLL映像,则退出
       Module->EntryPoint == 0)
     {
       return TRUE;
     }
   LdrpTlsCallback(Module, dwReason);//线程回调函数
   return  ((PDLLMAIN_FUNC)Module->EntryPoint)(Module->DllBase, dwReason, lpReserved);//对DLL映像发送DLL_THREAD_ATTACH消息
}

这里是专门针对DLL的,用来发送给DLL运行当DLL被线程附加的时候所进行的必要初始化,不过再这之前会调用TLS的回调函数。并且在LdrpAttachThread也调用了LdrpTlsCallback函数,有必要揭晓最后的秘密了。

static __inline VOID LdrpTlsCallback(PLDR_DATA_TABLE_ENTRY Module, ULONG dwReason)
{
   PIMAGE_TLS_CALLBACK *TlsCallback;
   if (Module->TlsIndex != 0xFFFF && Module->LoadCount == LDRP_PROCESS_CREATION_TIME)//若存在TLS变量
     {
       TlsCallback = LdrpTlsArray[Module->TlsIndex].TlsAddressOfCallBacks;//获取该模块相应的回调函数的指针数组
       if (TlsCallback)//如果该回调有效
         {
           while (*TlsCallback)//依次调用所有的回调函数
             {
               TRACE_LDR("%wZ - Calling tls callback at %xn",
                         &Module->BaseDllName, *TlsCallback);
               (*TlsCallback)(Module->DllBase, dwReason, NULL);//调用该回调函数
               TlsCallback++;
             }
         }
     }
}

这里就很简单了,首先获取TlsCallback的函数指针数组,然后依次调用它们。到此,TLS就分析完了,相比于之前,这次可以是相对简单,只要耐心分析源码,有很大收获


版权声明:本文为qq_40890756原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。