本文為線程本地存儲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數組的實現情況。抽絲剝繭,一目了然!