本文为线程本地存储TLS系列之实现探究。
我们在上一篇线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理中曾经说过TLS可以分为两类:静态TLS和动态TLS。然后又分别说明了两者在程序实现时的用法,并且还说明了windows对这两类TLS的实现原理,我们本文的目的是从底层实现的角度深入探究,深刻理解原理。
先考虑以下两个问题:
1、在上一篇中,我们说到静态TLS数据是在编译时放入.tls节,然后在系统加载程序时,会去寻找.tls节,并且分配一个足够大的内存空间来存放所有这些静态TLS变量。那么问题是,当程序加载后,对静态TLS数据分配的内存空间在哪里呢?用什么来表示呢?
2、在上一篇中,我们说到动态TLS是存放在每一个线程独立的TLS slot数组中,这个数组的大小是TLS_MINIMUM_AVAILABLE维,那么这个数组在哪里呢?TlsSetValue和TlsGetValue应该就是访问的这个数组,在取得索引的情况下,如果我们知道这个数组的位置,那么我们是否完全就能抛开上面两个函数自己读写来测试呢?
一、线程环境块TEB
在给出具体程序之前,我们有必要先讨论一下线程环境块TEB。
我们知道,每个线程都有属于自己的一系列数据,这些数据就是通过TEB来管理,当然就包括像TLS这样的线程私有数据。那么TEB的结构是什么样的呢?在微软的文档和头文件上,我没有查到TEB完整的信息,不过我们还是可以通过Windbg得到的,以下是TEB的详细展开信息:
nt!_TEB
0:001> dt -b nt!_TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x000 ExceptionList : Ptr32
+0x004 StackBase : Ptr32
+0x008 StackLimit : Ptr32
+0x00c SubSystemTib : Ptr32
+0x010 FiberData : Ptr32
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32
+0x018 Self : Ptr32
+0x01c EnvironmentPointer : Ptr32
+0x020 ClientId : _CLIENT_ID
+0x000 UniqueProcess : Ptr32
+0x004 UniqueThread : Ptr32
+0x028 ActiveRpcHandle : Ptr32
+0x02c ThreadLocalStoragePointer : Ptr32
+0x030 ProcessEnvironmentBlock : Ptr32
+0x034 LastErrorValue : Uint4B
+0x038 CountOfOwnedCriticalSections : Uint4B
+0x03c CsrClientThread : Ptr32
+0x040 Win32ThreadInfo : Ptr32
+0x044 User32Reserved : Uint4B
+0x0ac UserReserved : Uint4B
+0x0c0 WOW32Reserved : Ptr32
+0x0c4 CurrentLocale : Uint4B
+0x0c8 FpSoftwareStatusRegister : Uint4B
+0x0cc SystemReserved1 : Ptr32
+0x1a4 ExceptionCode : Int4B
+0x1a8 ActivationContextStack : _ACTIVATION_CONTEXT_STACK
+0x000 Flags : Uint4B
+0x004 NextCookieSequenceNumber : Uint4B
+0x008 ActiveFrame : Ptr32
+0x00c FrameListCache : _LIST_ENTRY
+0x000 Flink : Ptr32
+0x004 Blink : Ptr32
+0x1bc SpareBytes1 : UChar
+0x1d4 GdiTebBatch : _GDI_TEB_BATCH
+0x000 Offset : Uint4B
+0x004 HDC : Uint4B
+0x008 Buffer : Uint4B
+0x6b4 RealClientId : _CLIENT_ID
+0x000 UniqueProcess : Ptr32
+0x004 UniqueThread : Ptr32
+0x6bc GdiCachedProcessHandle : Ptr32
+0x6c0 GdiClientPID : Uint4B
+0x6c4 GdiClientTID : Uint4B
+0x6c8 GdiThreadLocalInfo : Ptr32
+0x6cc Win32ClientInfo : Uint4B
+0x7c4 glDispatchTable : Ptr32
+0xb68 glReserved1 : Uint4B
+0xbdc glReserved2 : Ptr32
+0xbe0 glSectionInfo : Ptr32
+0xbe4 glSection : Ptr32
+0xbe8 glTable : Ptr32
+0xbec glCurrentRC : Ptr32
+0xbf0 glContext : Ptr32
+0xbf4 LastStatusValue : Uint4B
+0xbf8 StaticUnicodeString : _UNICODE_STRING
+0x000 Length : Uint2B
+0x002 MaximumLength : Uint2B
+0x004 Buffer : Ptr32
+0xc00 StaticUnicodeBuffer : Uint2B
+0xe0c DeallocationStack : Ptr32
+0xe10 TlsSlots : Ptr32
+0xf10 TlsLinks : _LIST_ENTRY
+0x000 Flink : Ptr32
+0x004 Blink : Ptr32
+0xf18 Vdm : Ptr32
+0xf1c ReservedForNtRpc : Ptr32
+0xf20 DbgSsReserved : Ptr32
+0xf28 HardErrorsAreDisabled : Uint4B
+0xf2c Instrumentation : Ptr32
+0xf6c WinSockData : Ptr32
+0xf70 GdiBatchCount : Uint4B
+0xf74 InDbgPrint : UChar
+0xf75 FreeStackOnTermination : UChar
+0xf76 HasFiberData : UChar
+0xf77 IdealProcessor : UChar
+0xf78 Spare3 : Uint4B
+0xf7c ReservedForPerf : Ptr32
+0xf80 ReservedForOle : Ptr32
+0xf84 WaitingOnLoaderLock : Uint4B
+0xf88 Wx86Thread : _Wx86ThreadState
+0x000 CallBx86Eip : Ptr32
+0x004 DeallocationCpu : Ptr32
+0x008 UseKnownWx86Dll : UChar
+0x009 OleStubInvoked : Char
+0xf94 TlsExpansionSlots : Ptr32
+0xf98 ImpersonationLocale : Uint4B
+0xf9c IsImpersonating : Uint4B
+0xfa0 NlsCache : Ptr32
+0xfa4 pShimData : Ptr32
+0xfa8 HeapVirtualAffinity : Uint4B
+0xfac CurrentTransactionHandle : Ptr32
+0xfb0 ActiveFrame : Ptr32
+0xfb4 SafeThunkCall : UChar
+0xfb5 BooleanSpare : UChar
这个结构体包含的信息很多,我们此处只需要关注与TLS相关的,对其他的都忽略。所以我在程序里定义了以下这个结构体,用一些保留字段来控制相关域的偏移:
//通过windgb查看_TEB得到的我的系统(winXP+SP3)中的_TEB的实现
struct STEB
{
NT_TIB NtTib;
PVOID EnvironmentPointer;
//中间若干数据,与此处研究无关,故不展开,只标记偏移。下面的Reserved2,Reserved3也是同理
BYTE Reserved1[12];
PVOID ThreadLocalStoragePointer; //指向存放静态TLS数据的地址的指针的地址
BYTE Reserved2[3552];
PVOID TlsSlots[64]; //指向存放动态TLS数据的TLS Slot数组
BYTE Reserved3[132];
PVOID TlsExpansionSlots; //当索引大于63时,TlsSlots数组存不下了,就会新分配内存来存放,并且将指针记录在这里
//后面还有若干数据,与此处研究无关,故省略
};
以上就是我在之后两个程序中要用到的直接访问相关内存的线程TEB定义了。那么这个结构又是存放在哪里的呢?在windows系统上,该结构体的起始地址总是FS:[0]。而为了更方便的用指针访问,我们用到了NT_TIB结构中的Self字段,Self指针就是指向自身的其实地址,也就是NT_TIB的首地址,也就是我们的STEB的首地址。在winnt.h中,NT_TIB定义如下:
NT_TIB
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
#if defined(_MSC_EXTENSIONS)
union {
PVOID FiberData;
DWORD Version;
};
#else
PVOID FiberData;
#endif
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;
typedef NT_TIB *PNT_TIB;
如同_TEB一样,因为其他字段我们不关心,所以都不讨论了,此处只需要知道Self指针就是指向自身,有了他,可以方便的进行指针访问操作了。
二、静态TLS实现探究
我们通过下面的程序来研究静态TLS的实现。先说明程序的基本框架:在文件最开始,声明了3个静态TLS变量,并定义了要启动的线程数。然后main函数启动若干个线程,在线程函数中分别对3个TLS变量赋值,然后调用一个分析函数TlsMemFunc:这个函数用临界区来防止线程的输出相互干扰,首先用正常的方式打印出TLS变量的值,然后直接访问内存存放静态TLS变量的地方,自己获取相关的值打印出来。从这个过程中,我们可以深入探究windows对静态TLS内存管理的实现。程序如下,对关键部分都做了注释,就不再额外说明了,不过要格外留心的是TlsMemFunc中指针操作的代码,这是非常有趣的:
静态TLS研究程序
//本程序研究静态TLS,编译器会将程序中声明的所有静态TLS变量放到生成的PE文件的.tls段。
//系统加载可执行文件时,会为所有静态TLS数据分配内存,其中分配的内存地址由_TEB的ThreadLocalStoragePointer指出(需要一次转换)。
#include <stdio.h>
#include <windows.h>
#include <WinNT.h>
//通过windgb查看_TEB得到的我的系统(winXP+SP3)中的_TEB的实现
struct STEB
{
NT_TIB NtTib;
PVOID EnvironmentPointer;
//中间若干数据,与此处研究无关,故不展开,只标记偏移。下面的Reserved2,Reserved3也是同理
BYTE Reserved1[12];
PVOID ThreadLocalStoragePointer; //指向存放静态TLS数据的地址的指针的地址
BYTE Reserved2[3552];
PVOID TlsSlots[64]; //指向存放动态TLS数据的TLS Slot数组
BYTE Reserved3[132];
PVOID TlsExpansionSlots; //当索引大于63时,TlsSlots数组存不下了,就会新分配内存来存放,并且将指针记录在这里
//后面还有若干数据,与此处研究无关,故省略
};
//设定启动线程数
#define THREADCOUNT 5
int baseorder=0;
//静态TLS数据
__declspec(thread) DWORD dwRunOrder=0;
__declspec(thread) DWORD dwThreadID=0;
__declspec(thread) char chThreadName[50]={0};
//为了查看内存数据时正常,采用临界区
CRITICAL_SECTION gDisplayTLS_CS;
//内存中查看静态TLS数据
VOID TlsMemFunc(VOID)
{
EnterCriticalSection( &gDisplayTLS_CS );
printf("=============================原始静态TLS数据=============================\n");
printf("dwThreadID=%d;dwRunOrder=%d;chThreadName=%s\n",dwThreadID,dwRunOrder,chThreadName);
printf("=========================================================================\n");
PNT_TIB pTIB;
STEB * pTEB;
__asm
{
mov EAX, FS:[18h]
mov [pTIB], EAX
}
pTEB = (STEB *)pTIB;
printf("=======================通过内存偏移查看静态TLS数据=======================\n");
//pTEB->ThreadLocalStoragePointer指向存放静态TLS数据的地址的指针的地址
DWORD dwTrueOffset = *(DWORD *)(pTEB->ThreadLocalStoragePointer);
//将上面的值转为地址,就是指向真正存放静态TLS数据的内存的指针
DWORD * pFirst = (DWORD*)dwTrueOffset;
#ifdef _DEBUG
//注意,在debug版本下,因为系统会自动分配一段很大的内存在存放调试信息,所以TLS数据位置会延后!
//在我的编译器vs2010中,是分配了0x100个字节,但不能保证在别的环境下也是这样,所以建议用Release编译得到的可执行文件!!!
pFirst += 64; //0x100个字节
#endif
//这块内存总是由4个0字节组成,也就是一个DWORD.
++pFirst;
DWORD dwMemRunOrder=*(pFirst+1);
DWORD dwMemThreadID=*pFirst;
char * pMemThreadName = (char*)(pFirst+2);
printf("dwThreadID=%d;dwRunOrder=%d;chThreadName=%s\n",dwMemThreadID,dwMemRunOrder,pMemThreadName);
printf("=========================================================================\n\n");
LeaveCriticalSection( &gDisplayTLS_CS );
}
DWORD WINAPI ThreadFunc(LPVOID lpThreadParameter)
{
dwRunOrder = ++baseorder;
dwThreadID = GetCurrentThreadId();
strcpy(chThreadName,(char*)lpThreadParameter);
TlsMemFunc();
return 0;
}
DWORD main(VOID)
{
DWORD IDThread;
HANDLE hThread[THREADCOUNT];
char name[THREADCOUNT][50]={0};
InitializeCriticalSection( &gDisplayTLS_CS );
// Create multiple threads.
for (int i = 0; i < THREADCOUNT; i++)
{
sprintf(name[i],"This thread is number %d!",i+1);
hThread[i] = CreateThread(NULL, // default security attributes
0, // use default stack size
(LPTHREAD_START_ROUTINE) ThreadFunc, // thread function
name[i], // no thread function argument
0, // use default creation flags
&IDThread); // returns thread identifier
// Check the return value for success.
if (hThread[i] == NULL)
fprintf(stderr, "CreateThread %d error\n", i);
}
for (int i = 0; i < THREADCOUNT; i++)
WaitForSingleObject(hThread[i], INFINITE);
DeleteCriticalSection( &gDisplayTLS_CS );
return 0;
}
要说明的是:由于debug版本,编译器会自动分配一段内存来存放调试信息,从上面的代码中,可以看到我已经对我的编译环境VS2010进行了代码调整(_DEBUG宏部分),但是如果你用的是别的编译器,那我不确定他分配的是和VS2010一样的,所以可能结果不正确,建议用Release版本来分析。
程序运行结果如下:
可以看出,通过直接访问静态TLS变量和访问TEB中相关内存得到的是一样的,由此我们就更加深入的理解了上一篇中讲的静态TLS的原理,也知道操作系统是如何管理和实现静态TLS的。
三、动态TLS实现探究
我们通过下面的程序来研究动态TLS的实现。本程序的基本框架和静态TLS程序结构大致相同,但是不需要声明静态TLS变量了,而是用TlsXXX系列函数来创建动态TLS数据。然后关键是分析函数TlsMemFunc:这个函数用临界区来防止线程的输出相互干扰,首先用TlsGetValue的方式打印出TLS变量的值,然后直接访问内存读取动态TLS变量并打印出来。要特别注意的是:TEB中存放动态TLS的数组只有64维,但是windowsNT支持1000多个TLS数据,这是利用扩展Slot指针来实现的,为了模拟这种情况,我特意在main中调用TlsAlloc直至索引超过64,而需要访问扩展空间的情况。这些代码很有趣,读者可以自己尝试修改观察。从这个过程中,我们可以深入探究windows对动态TLS内存管理的实现。程序的关键部分都做了注释,就不再额外展开了,不过与上面的程序一样,请一定要细心研究TlsMemFunc对指针操作的代码:
动态TLS研究
//本程序研究动态TLS,动态TLS是通过调用一组API实现,在生成的PE文件中没有.tls段。
//可执行文件运行后,没新建一个线程,都会为这个线程创建一个数组TLS Slot,用于存放动态TLS数据。该数组就是由_TEB的TlsSlots指出。
//TlsSetValue和TlsGetValue其实也就是访问的这个数组,当维数超过64时,由于TlsSlots不够,所以用到了TlsExpansionSlots
#include <stdio.h>
#include <windows.h>
#include <WinNT.h>
//通过windgb查看_TEB得到的我的系统(winXP+SP3)中的_TEB的实现
struct STEB
{
NT_TIB NtTib;
PVOID EnvironmentPointer;
//中间若干数据,与此处研究无关,故不展开,只标记偏移。下面的Reserved2,Reserved3也是同理
BYTE Reserved1[12];
PVOID ThreadLocalStoragePointer; //指向存放静态TLS数据的地址的指针的地址
BYTE Reserved2[3552];
PVOID TlsSlots[64]; //指向存放动态TLS数据的TLS Slot数组
BYTE Reserved3[132];
PVOID TlsExpansionSlots; //当索引大于63时,TlsSlots数组存不下了,就会新分配内存来存放,并且将指针记录在这里
//后面还有若干数据,与此处研究无关,故省略
};
//设定启动线程数
#define THREADCOUNT 5
int baseorder=0;
//TLS Slot索引
DWORD dwTlsIndex1,dwTlsIndex2;
//为了查看内存数据时正常,采用临界区
CRITICAL_SECTION gDisplayTLS_CS;
//内存中查看动态TLS数据
VOID TlsMemFunc(VOID)
{
EnterCriticalSection( &gDisplayTLS_CS );
printf("=============================原始动态TLS数据=============================\n");
printf("dwThreadID=%d;threadinfo=%s\n",(DWORD)TlsGetValue(dwTlsIndex1),(char*)TlsGetValue(dwTlsIndex2));
printf("=========================================================================\n");
PNT_TIB pTIB;
STEB * pTEB;
__asm
{
mov EAX, FS:[18h]
mov [pTIB], EAX
}
pTEB = (STEB *)pTIB;
printf("=======================通过内存偏移查看动态TLS数据=======================\n");
DWORD dwTlsIndex1Value,dwTlsIndex2Value;
if (dwTlsIndex1 < 64)
{
dwTlsIndex1Value = (DWORD)pTEB->TlsSlots[dwTlsIndex1];
}
else
{
DWORD * pES = (DWORD*)(pTEB->TlsExpansionSlots);
pES += dwTlsIndex1-64;
dwTlsIndex1Value = *pES;
}
if (dwTlsIndex2 < 64)
{
dwTlsIndex2Value = (DWORD)pTEB->TlsSlots[dwTlsIndex2];
}
else
{
DWORD * pES = (DWORD*)(pTEB->TlsExpansionSlots);
pES += dwTlsIndex2-64;
dwTlsIndex2Value = (*pES);
}
printf("dwThreadID=%d;threadinfo=%s\n",dwTlsIndex1Value,(char*)dwTlsIndex2Value);
printf("=========================================================================\n\n");
LeaveCriticalSection( &gDisplayTLS_CS );
}
DWORD WINAPI ThreadFunc(VOID)
{
TlsSetValue(dwTlsIndex1,(LPVOID)GetCurrentThreadId());
char info[100]={0};
sprintf(info,"My threadID is %d,My runorder is %d!",GetCurrentThreadId(),++baseorder);
TlsSetValue(dwTlsIndex2,info);
TlsMemFunc();
return 0;
}
DWORD main(VOID)
{
DWORD IDThread;
HANDLE hThread[THREADCOUNT];
InitializeCriticalSection( &gDisplayTLS_CS );
dwTlsIndex1 = TlsAlloc();
//故意使索引超过64,而测试扩展slot
while(dwTlsIndex1 < 63)
dwTlsIndex1 = TlsAlloc();
dwTlsIndex2 = TlsAlloc();
if (dwTlsIndex1 == TLS_OUT_OF_INDEXES || dwTlsIndex2 == TLS_OUT_OF_INDEXES)
{
fprintf(stderr, "TlsAlloc failed\n");
return -1;
}
printf("dwTlsIndex1=%d:用于存放线程ID;dwTlsIndex2=%d:用于存放指向线程信息的指针。\n", dwTlsIndex1, dwTlsIndex2);
// Create multiple threads.
for (int i = 0; i < THREADCOUNT; i++)
{
hThread[i] = CreateThread(NULL, // default security attributes
0, // use default stack size
(LPTHREAD_START_ROUTINE) ThreadFunc, // thread function
NULL, // no thread function argument
0, // use default creation flags
&IDThread); // returns thread identifier
// Check the return value for success.
if (hThread[i] == NULL)
fprintf(stderr, "CreateThread %d error\n", i);
}
for (int i = 0; i < THREADCOUNT; i++)
WaitForSingleObject(hThread[i], INFINITE);
TlsFree(dwTlsIndex1);
TlsFree(dwTlsIndex2);
DeleteCriticalSection( &gDisplayTLS_CS );
return 0;
}
程序运行结果如下:
同样可以看出,通过TlsGetValue访问动态TLS变量和访问TEB中相关内存得到的是一样的,由此我们就更加深入的理解了上一篇中讲的动态TLS的原理,已经底层的Tls slots数组的实现情况。抽丝剥茧,一目了然!