1.简介
TLS线程局部存储在每个线程的基础上有效地存储状态,可以在一定程度上避免在每个线程的基础上实例化全局变量的情况。
TLS的访问是通过TEB上存在的指针或数组
2.基础使用
2.1 显示使用API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <Windows.h>
DWORD g_tlsIdx = 0;
DWORD WINAPI ThreadProc1( LPVOID lpThreadParameter ) { TlsSetValue(g_tlsIdx, (LPVOID)1); Sleep(2000); printf("thread1 val: %x\r\n", (DWORD)TlsGetValue(g_tlsIdx)); return 0; }
DWORD WINAPI ThreadProc2( LPVOID lpThreadParameter ) { TlsSetValue(g_tlsIdx, (LPVOID)2); Sleep(1000); printf("thread2 val: %x\r\n", (DWORD)TlsGetValue(g_tlsIdx)); return 0; }
int main(int argc, char* argv[]) {
g_tlsIdx = TlsAlloc();
HANDLE hThread1 = ::CreateThread(0, 0, ThreadProc1, 0, 0, 0); HANDLE hThread2 = ::CreateThread(0, 0, ThreadProc2, 0, 0, 0);
WaitForSingleObject(hThread1, -1); WaitForSingleObject(hThread2, -1);
TlsFree(g_tlsIdx);
printf("threads all exit.\r\n");
return 0; }
|
2.2 隐式使用
允许使用TLS和全局变量一样方便,需要编译器支持。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include <windows.h>
__declspec(thread) int threadedint = 100;
DWORD WINAPI ThreadProc1( LPVOID lpThreadParameter ) { threadedint = 1; Sleep(2000); printf("thread1 val: %x\r\n", threadedint); return 0; }
DWORD WINAPI ThreadProc2( LPVOID lpThreadParameter ) { threadedint = 2; Sleep(1000); printf("thread2 val: %x\r\n", threadedint); return 0; }
void NTAPI myTLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved) { printf("tls callback reason: %d\r\n", Reason); }
#pragma section(".CRT$XLY",long,read) extern "C" __declspec(allocate(".CRT$XLY")) PIMAGE_TLS_CALLBACK _xl_y = myTLS_CALLBACK;
int main(int argc, char* argv[]) {
HANDLE hThread1 = ::CreateThread(0, 0, ThreadProc1, 0, 0, 0); HANDLE hThread2 = ::CreateThread(0, 0, ThreadProc2, 0, 0, 0);
WaitForSingleObject(hThread1, -1); WaitForSingleObject(hThread2, -1); return 0; }
|
3.实现原理
3.1 API原理
TEB 中的 TlsSlots 数组是每个线程的一部分,它为每个线程提供了一组有保证的 64 个线程本地存储插槽。后来,Microsoft 认为 64 个 TLS 插槽不够用,并添加了 TlsExpansionSlots 指针(指向堆),以获得额外的 1024 个 TLS 插槽。如果超过初始的 64 个插槽集,TlsExpansionSlots 将在 TlsAlloc 中按需分配。
a.TlsAlloc
在全局位置调用这个函数来获得一个可用的插槽索引,如果前 64 个插槽已经用完,会分配堆内存。
1 2 3 4
| DWORD TlsAlloc();
如果函数成功,则返回值为 TLS 索引。索引的插槽初始化为零。 如果函数失败,则返回值为 TLS_OUT_OF_INDEXES(-1)。调用GetLastError获取拓展错误信息。
|
反汇编如下:

b.TlsSetValue
1 2 3 4 5 6 7
| BOOL TlsSetValue( [in] DWORD dwTlsIndex, [in, optional] LPVOID lpTlsValue );
如果函数成功,则返回值为非零值。 如果函数失败,则返回值为零。调用 GetLastError获取拓展错误信息。
|
反汇编如下:

c.TlsGetValue
1 2 3 4 5 6 7
| LPVOID TlsGetValue( [in] DWORD dwTlsIndex );
如果函数成功,则返回值是存储在与指定索引关联的调用线程的 TLS 槽中的值。如果 dwTlsIndex 是通过成功调用 TlsAlloc 分配的有效索引,则此函数始终成功。 如果函数失败,则返回值为零。若要获取扩展的错误信息,请调用 GetLastError。 存储在 TLS 槽中的数据的值可以设置为 0,因为它仍具有其初始值,或者因为调用 TlsSetValue 函数的线程为 0。因此,如果返回值为 0,则必须在确定函数失败之前检查 GetLastError 是否返回ERROR_SUCCESS。如果 GetLastError 返回 ERROR_SUCCESS,则函数已成功,TLS 槽中存储的数据为 0。否则,函数将失败。
|
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| PVOID WINAPI TlsGetValue( __in DWORD dwTlsIndex ) { PTEB Teb = NtCurrentTeb(); Teb->LastErrorValue = 0; if (dwTlsIndex < 64) return Teb->TlsSlots[ dwTlsIndex ];
if (dwTlsIndex > 1088) { BaseSetLastNTError( STATUS_INVALID_PARAMETER ); return 0; } if (!Teb->TlsExpansionSlots) return 0; return Teb->TlsExpansionSlots[ dwTlsIndex - 64 ]; }
|
d.TlsFree
1 2 3 4 5 6
| BOOL TlsFree( [in] DWORD dwTlsIndex );
果该函数成功,则返回值为非零值。 如果函数失败,则返回值为零。 调用 GetLastError 获得更多的错误信息。
|
反汇编:

3.2 隐式原理
a.PE结构
当使用__declspec(thread)
声明了一个线程局部变量时,编译器和链接器会在.rdata
中隐式添加_tls_used
变量,变量类型如下:
1 2 3 4 5 6 7 8 9 10
| _CRTALLOC(".rdata$T") const IMAGE_TLS_DIRECTORY _tls_used = { (ULONG)(ULONG_PTR) &_tls_start, (ULONG)(ULONG_PTR) &_tls_end, (ULONG)(ULONG_PTR) &_tls_index, (ULONG)(ULONG_PTR) (&__xl_a+1), (ULONG) 0, (ULONG) 0 };
|
这个其实就是PE结构中TLS页目录表中指向的结构。
b.回调函数
CRT 代码还提供了一种机制,允许程序注册一组 TLS 回调,这些回调是具有与 DllMain 类似原型的函数,当线程在当前进程中启动或退出(正常地)时调用。 (这些回调甚至可以为没有 DllMain 例程的主进程映像注册。)回调的类型为 PIMAGE_TLS_CALLBACK
,并且 TLS 目录指向一个以 null 结尾的回调数组(按顺序调用)。要使用 CRT 为 TLS 回调提供的支持,需要声明一个存储在特别命名的.CRT$XLx
部分中的变量,其中 x 是 A 和 Z 之间的值。这样做是保证链接后函数的顺序不会变。
1 2 3
| #pragma section(".CRT$XLY",long,read) extern "C" __declspec(allocate(".CRT$XLY")) PIMAGE_TLS_CALLBACK _xl_y = MyTlsCallback;
|
c.访问数据
测试用例如下:
1 2 3 4 5 6 7 8
| __declspec(thread) int threadedint = 100;
int main() { threadedint = 1;
return 0; }
|
反汇编如下:
为tls变量赋值:

_tls_start区域数据:

经过调试分析发现TEB->ThreadLoacalStoragePointer
是一个指向堆内存的数组指针,根据_tls_index
的值进行偏移,得到针对当前线程所在的一个_tls_start
数据区域,再根据eax
的值来访问具体的tls变量。