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

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

📁 漫谈系统内核内幕 收集得很辛苦 呵呵 大家快下在吧
💻 TXT
📖 第 1 页 / 共 3 页
字号:
    __asm__ __volatile__ (
        "movl %%fs:0x18, %0\n"
        : "=r" (ret)
        : /* no inputs */
    );
    return ret;
}

    当CPU运行于用户空间时,段寄存器总是(通过相应的段描述项)指向当前线程的TEB,TEB的起点就是段的起点。TEB中位移为0x18处是个指针Self,指向其所在TEB的起点,所以这里的%%fs:0x18就是当前TEB的起点(注意%%fs:0是TEB起点处的内容,但是不能以&(%%fs:0)取起点的地址,也不存在可以直接达到这个目的的指令)。
    由TlsAlloc()返回的索引号、即下标、代表着一个动态TLS变量,一般都是保存在一个全局量或静态变量中,为同一进程中的所有线程所共见和共用。但是不同线程在使用这个共同下标时访问的数组是不同的。当然,若要将下标保存在一个局部变量中也并无不可,但是那样就浪费了一个TLS变量,因为局部变量本来就已经是局部的、不能为其它线程所见。
    需要读/写这个动态TLS变量的时候,就使用这个下标。作为TlsSetValue()或TlsGetValue()的参数之一,其作用就像是目标TLS变量的地址。于是,不同的线程使用相同的地址,访问的却是不同的变量、即局部于不同线程的变量。
    明白了这些,函数TlsSetValue()的代码就是直截了当的了:

BOOL STDCALL
TlsSetValue(DWORD dwTlsIndex, LPVOID lpTlsValue)
{
   if (dwTlsIndex >= TLS_MINIMUM_AVAILABLE)
   {
     SetLastErrorByStatus(STATUS_INVALID_PARAMETER);
     return(FALSE);
   }
   NtCurrentTeb()->TlsSlots[dwTlsIndex] = lpTlsValue;
   return(TRUE);
}

    明白了TlsAlloc()和TlsSetValue()的代码,TlsGetValue()和TlsFree()的代码就更是不值一提。可见,动态TLS的实现相当简洁。

    值得一提的倒是TLS的初始化。
    TLS的初始化可以分成两部分,即静态TLS的初始化和动态TLS的初始化。可想而知,动态TLS的初始化是很简单的,而静态TLS的初始化则比较复杂。
    我们从__true_LdrInitializeThunk()开始看TLS的初始化,因为新创建的线程都是从这里开始执行的。特别地,我们从创建进程时主线程、即第一个线程的初始化开始。以前在介绍装入/执行PE可执行文件和创建进程/线程的过程时讲解过这个函数,但那时还没有到要关心TLS的地步,所以当时都把与TLS有关的代码跳过了。

VOID STDCALL
__true_LdrInitializeThunk (ULONG Unknown1, ULONG Unknown2,
                         ULONG Unknown3, ULONG Unknown4)
{
   . . . . . .

   DPRINT("LdrInitializeThunk()\n");
   if (NtCurrentPeb()->Ldr == NULL || NtCurrentPeb()->Ldr->Initialized == FALSE)
   {
       Peb = (PPEB)(PEB_BASE);
       DPRINT("Peb %x\n", Peb);
       ImageBase = Peb->ImageBaseAddress;
       . . . . . .
       . . . . . .
       /* initialize tls bitmap */
       RtlInitializeBitMap (&TlsBitMap, Peb->TlsBitmapBits,
                            TLS_MINIMUM_AVAILABLE);
       Peb->TlsBitmap = &TlsBitMap;
       Peb->TlsExpansionCounter = TLS_MINIMUM_AVAILABLE;
       . . . . . .
       NtModule->TlsIndex = -1;
       . . . . . .
       EntryPoint = LdrPEStartup((PVOID)ImageBase, NULL, NULL, NULL);
       ExeModule->EntryPoint = (ULONG)EntryPoint;
       . . . . . .
   }
   /* attach the thread */
   RtlEnterCriticalSection(NtCurrentPeb()->LoaderLock);
   LdrpAttachThread();
   RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);
}

    进程中的第一个线程需要执行if语句中的内容,以后的线程就跳过这一部分,只是执行后面的LdrpAttachThread()了。
    RtlInitializeBitMap()的作用就是建立本进程的动态TLS,即TLS位图。这里的TlsBitMap是个RTL_BITMAP数据结构,但是这个数据结构里面并不包括实际用作位图的缓冲区,这缓冲区就是Peb->TlsBitmapBits,位图的大小则是TLS_MINIMUM_AVAILABLE,即64。设置好TlsBitMap以后,将其指针填写到PEB中,并相应设置PEB的其它几个有关的字段,本进程的动态TLS机制就建立起来了,可见动态TLS的初始化是很简单的。
    下面是LdrPEStartup(),这就涉及静态TLS的初始化了。先要介绍一下调用这个函数的背景。此时目标EXE映像和ntdll.dll的映像均已装入,现在CPU是在ntdll.dll的映像中运行。本进程PEB中的指针Ldr指向一个PEB_LDR_DATA数据结构,这个数据结构中有个“已装入模块”队列InLoadOrderModuleList,里面是代表着已装入模块的LDR_MODULE数据结构,此刻这个队列中已经有了两个模块。

[__true_LdrInitializeThunk() > LdrPEStartup()]

PEPFUNC LdrPEStartup (PVOID ImageBase, HANDLE SectionHandle,
                      PLDR_MODULE* Module, PWSTR FullDosName)
{
   . . . . . .
   .PLDR_MODULE tmpModule;

   . . . . . .
   if (Module != NULL)
   {
       *Module = LdrAddModuleEntry(ImageBase, NTHeaders, FullDosName);
       (*Module)->SectionHandle = SectionHandle;
   }
   else
   {
       Module = &tmpModule;
       Status = LdrFindEntryForAddress(ImageBase, Module);
       if (!NT_SUCCESS(Status))
       {
           return NULL;
       }
   }
   . . . . . .

   /*
    * If the DLL's imports symbols from other modules, fixup the imported calls entry points.
    */
   DPRINT("About to fixup imports\n");
   Status = LdrFixupImports(NULL, *Module);
   . . . . . .
   Status = LdrpInitializeTlsForProccess();
   if (NT_SUCCESS(Status))
   {
     Status = LdrpAttachProcess();
   }
   . . . . . .

   /* Compute the DLL's entry point's address. */
   . . . . . .
   return EntryPoint;
}

    调用参数ImageBase指向EXE映像装入本进程用户空间后的地址。
    回过去看一下从__true_LdrInitializeThunk()里面调用这个函数时的实际参数,就可知道此时除ImageBase以外的其余三个参数、包括Module、都是NULL。所以,这里使用一个临时的LDR_MODULE结构指针tmpModule,通过LdrFindEntryForAddress()从“已装入模块队列”中根据地址ImageBase找到其所属的模块,并返回其LDR_MODULE结构指针。当然,这就是EXE模块。然后就以此为参数调用LdrFixupImports(),从EXE模块开始,通过有可能是递归的LdrFixupImports()调用,完成对所有DLL模块的装入和动态连接(只有ntdll.dll已经装入)。而在连接的过程中,就可能要涉及其.tls段了。
    注意这里调用LdrFixupImports()时的参数是*Module,而不是Module。Module的类型定义是“PLDR_MODULE* Module”,所以是双重指针,但是tmpModule的类型却是LDR_MODULE指针。
    下面就是从EXE模块开始执行LdrFixupImports()的过程了:

[__true_LdrInitializeThunk() > LdrPEStartup() > LdrFixupImports()]

static NTSTATUS
LdrFixupImports(IN PWSTR SearchPath OPTIONAL, IN PLDR_MODULE Module)
{
   . . . . . .

   /* Check for tls data */
   TlsDirectory = (PIMAGE_TLS_DIRECTORY)
                  RtlImageDirectoryEntryToData(Module->BaseAddress, TRUE,
                               IMAGE_DIRECTORY_ENTRY_TLS, NULL);
   if (TlsDirectory)
   {
       TlsSize = TlsDirectory->EndAddressOfRawData
                   - TlsDirectory->StartAddressOfRawData
                   + TlsDirectory->SizeOfZeroFill;
       if (TlsSize > 0 && NtCurrentPeb()->Ldr->Initialized)
       {
           TRACE_LDR("Trying to load dynamicly %wZ which contains a tls directory\n",
                     &Module->BaseDllName);
           return STATUS_UNSUCCESSFUL;
       }
   }

   . . . . . .

   if (TlsDirectory && TlsSize > 0)
   {
       LdrpAcquireTlsSlot(Module, TlsSize, FALSE);
   }
   return STATUS_SUCCESS;
}

    LdrFixupImports()的主要作用固然是完成对DLL的装入和动态连接(可能需要递归),却同时也兼顾了对于静态TLS的初始化处理。这里一开始就检查当前模块(在这一层是EXE模块)的映像中是否有.tls段、即类型为IMAGE_DIRECTORY_ENTRY_TLS的段。如果有的话,就根据映像头部所提供的信息计算出.tls段的长度。当然,我们在这里只关心.tls段长度非0的情况。
    有意思的是,如果(整个进程的)初始化已经完成,此时就提示“试图动态装入含有TLS目录的XX模块”并出错返回。注意这里讲的是动态装入、而不是动态连接。DLL模块一般是在创建进程、启动执行EXE映像的时候装入的,那是静态装入;但是也可以在运行的过程中根据需要随时装入,那就是动态装入。不管是静态装入还是动态装入都需要动态连接,所以都需要调用LdrFixupImports()。前面讲过,把动态装入的模块的.tls段合并到已在运行中的进程的TLS机制中是个比较麻烦的事。显然,至少ReactOS目前尚未实现此项功能;Windows怎么样不好说,估计也是一样。另一方面,这也解释了为什么有关的资料都鼓励在DLL中使用动态TLS、而不鼓励使用静态TLS。
    对于静态装入的DLL,则后面通过LdrpAcquireTlsSlot()为其分配一个非负的索引序号TlsIndex,并将其.tls段的大小纳入统计:

[__true_LdrInitializeThunk() > LdrPEStartup() > LdrFixupImports() > LdrpAcquireTlsSlot()]

static inline
VOID LdrpAcquireTlsSlot(PLDR_MODULE Module, ULONG Size, BOOLEAN Locked)
{
   if (!Locked)
   {
       RtlEnterCriticalSection (NtCurrentPeb()->LoaderLock);
   }
   Module->TlsIndex = (SHORT)LdrpTlsCount;
   LdrpTlsCount++;
   LdrpTlsSize += Size;
   if (!Locked)
   {
       RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);
   }
}

    显然,LdrpTlsCount表示.tls段的个数,而LdrpTlsSize表示累计的大小,将来要根据这两项数据为本进程建立静态TLS。
    对于LdrFixupImports()的调用有可能(更确切地说是必然)是递归的,对于启动执行目标EXE映像所涉及的每个模块都会调用一次,所以最后就把所有.tls段的大小都统计进来了。当CPU从最外层针对EXE模块的LdrFixupImports()返回时,LdrpTlsSize中已经有了总计的TLS大小。
    回到LdrPEStartup()的代码,下一步是对于LdrpInitializeTlsForProccess()的调用。

[__true_LdrInitializeThunk() > LdrPEStartup() > LdrpInitializeTlsForProccess()]

static NTSTATUS
LdrpInitializeTlsForProccess(VOID)
{
  PLIST_ENTRY ModuleListHead;
  PLIST_ENTRY Entry;
  PLDR_MODULE Module;
  PIMAGE_TLS_DIRECTORY TlsDirectory;
  PTLS_DATA TlsData;

⌨️ 快捷键说明

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