Windows线程本地储存_TLS分析

一、何为TLS

ThreadLocalStore(TLS),线程本地存储。PE文件中一种特殊的数据存储方式,同时也是Windows操作系统提供的一种机制,允许每个线程拥有自己的数据区,而不是共享同一个全局变量。在多线程模式下,有些变量需要支持每个线程独享一份的功能,这种每个线程独享的变量会放到每个线程专有的存储区域,允许每个线程拥有自己单独的变量实例,简而言之,我们可以说每个线程都可以有自己独立的变量实例,而不会干扰其他线程,在多线程环境下,使用TLS来实现线程私有的数据存储,确保了数据的安全性和隔离性。

二、例子分析

2.1 线程数据

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
#include <stdio.h>
#include <Windows.h>

__declspec(thread) int g_mydata = 1;

DWORD WINAPI thread_proc(LPVOID a)
{
while (TRUE)
{
++g_mydata;
printf("[%d] g_mydata ptr = %p, value = %d\n", GetCurrentThreadId(), &g_mydata, g_mydata);
Sleep(1000);
}
return 0;
}

int main()
{
CloseHandle(CreateThread(NULL, 0, thread_proc, NULL, 0, NULL));
Sleep(2000);
CloseHandle(CreateThread(NULL, 0, thread_proc, NULL, 0, NULL));
while (TRUE)
{
Sleep(1000);
}
return 0;
}

代码中使用了__declspec(thread)去修饰变量,指示编译器为每个线程创建此变量的独立副本。这是使用TLS的最简单方式,编译器会自动处理TLS数据段的创建,运行后查看进程效果:

可以看到两个线程中,g_mydata的地址和数据都互不相同,且不干扰。

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
42
43
44
45
46
47
#include <stdio.h>
#include <Windows.h>

#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback")
#endif

void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
char szMsg[80] = { 0, };
sprintf_s(szMsg, sizeof(szMsg), "[%d]TLS_CALLBACK1() : DllHandle = %p, Reason = %d", GetCurrentThreadId(), DllHandle, Reason);
printf("%s\n", szMsg);
}

EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif

//存储回调函数地址
PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK1,0 };

#pragma data_seg ()
#pragma const_seg ()

DWORD WINAPI thread_proc(LPVOID a)
{
printf("thread_proc = %d\n", GetCurrentThreadId());
return 0;
}

int main()
{
printf("main enter\n");
CloseHandle(CreateThread(NULL, 0, thread_proc, NULL, 0, NULL));
Sleep(2000);
CloseHandle(CreateThread(NULL, 0, thread_proc, NULL, 0, NULL));
getchar();
return 0;
}

这里解释一下代码:

  • 4-10:这里使用条件编译来处理32位和64位的差异(注意区分下划线)。

    • /INCLUDE:__tls_used/INCLUDE:_tls_used用于强制链接器包含TLS目录,即使没有引用它。

    • /INCLUDE:__tls_callback/INCLUDE:_tls_callback:强制链接器包含TLS回调函数表。(因为下边有注册TLS函数)

  • 12~17:TLS回调函数的实现,三个参数与DllMain同对应。

  • 19~25:这里开始创建TLS回调函数表:

    • EXTERN_C:确保使用C语言链接约定,因为C++编译后函数为别名,导致后续无法识别真正的TLS函数名。

    • .CRT$XLB:特殊的段名,Windows加载器会识别此段中的TLS回调函数。

    • _tls_callback[]:回调函数指针数组,以NULL(0)结尾。

    • 64位平台使用常量段(const_seg),32位平台使用数据段(data_seg)

  • 33~37:普通线程,调用printf输出线程ID。

  • 39~46:main函数创建两个线程,创建前后间隔2秒,然后通过getchar卡住程序。


运行代码看看效果:

Reason从MSDN给出的定义如下:

1
2
3
4
#define DLL_PROCESS_ATTACH   1    
#define DLL_THREAD_ATTACH 2
#define DLL_THREAD_DETACH 3
#define DLL_PROCESS_DETACH 0

因此可以得出结论:TLS回调执行的时机在线程创建时结束时。由于main函数本身就是一个主线程,因此在main函数执行前,触发了一次TLS回调,但为什么main线程结束后,没有调用TLS?这个在后边分析会知道。

三、TLS分析

TLS_CALLBACK1函数添加一个int3断点,然后使用windbg挂起。

断点触发后,查看堆栈。

发现顶层的来源为ntdll!LdrInitializeThunk,这个函数如果分析过NtCreateThread函数朋友就知道,这是线程函数的真正入口,并且还是一个APC函数。NtCreateThread调用后会构造TEB和其他一些列数据,最后插入APC进行触发线程执行。这里我们并不关心内核是怎么给APC插入的,我们只关心TLS是怎么被触发并调用的。