📄 漫谈兼容内核之二十三关于tls.txt
字号:
__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 + -