TLS线程局部存储

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) //TLS回调函数
{
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获取拓展错误信息。

反汇编如下:

TlsAlloc反汇编

b.TlsSetValue

1
2
3
4
5
6
7
BOOL TlsSetValue(
[in] DWORD dwTlsIndex,
[in, optional] LPVOID lpTlsValue
);

如果函数成功,则返回值为非零值。
如果函数失败,则返回值为零。调用 GetLastError获取拓展错误信息。

反汇编如下:

TlsSetValue反汇编

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(); // fs:[0x18]
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 获得更多的错误信息。

反汇编:

TlsFree反汇编

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, // tls中初始化的变量起始rva
(ULONG)(ULONG_PTR) &_tls_end, // tls中初始化的变量结束rva
(ULONG)(ULONG_PTR) &_tls_index, // tls插槽索引rva,区分不同的模块
(ULONG)(ULONG_PTR) (&__xl_a+1), // callbacks函数指针起始rva
(ULONG) 0, // size of tls zero fill
(ULONG) 0 // characteristics
};

这个其实就是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变量

_tls_start区域数据:

_tls_start

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


TLS线程局部存储
http://helloymf.github.io/2023/05/04/tls-xian-cheng-ju-bu-cun-chu/
作者
JNZ
许可协议