一、TLS概念
线程局部存储(Thread Local Storage,TLS)是一种线程级别的存储机制,它允许每个线程在运行时都拥有自己的私有变量,这些变量只能被该线程访问,而不会被其他线程所共享。
1、TLS的出现是为了解决什么问题呢?
》在多线程编程中,使用线程局部存储可以避免竞争条件和锁等同步机制的开销,提高程序的性能和并发能力。线程局部存储通常通过使用操作系统提供的API来实现,比如Windows平台上的TLS函数,或者POSIX线程库中的pthread_key_create和pthread_setspecific函数等。
线程局部存储通常被用于存储与线程相关的上下文信息,比如线程ID、日志记录器、线程池等资源的指针、临时变量等。在多线程环境中,使用线程局部存储可以避免共享全局变量带来的线程安全问题,同时也可以使代码更加简洁和可读,提高代码的可维护性。但是对于像飞机票抢票程序中,多个线程需要同时访问飞机票的数目,而且需要对飞机票数目进行修改和更新。如果使用TLS变量来进行管理,每个线程都会拥有自己的私有飞机票数目,无法实现多个线程之间对飞机票数目的共享和同步,也就无法实现正确的抢票功能。因此,使用全局变量或共享内存等线程间共享的方式来管理飞机票数目,是更为合适的选择。
2、为什么使用线程局部存储可以避免竞争条件和锁等同步机制的开销呢?
1)线程局部存储是每个线程私有的,不同线程之间互相独立,因此不需要进行同步机制的操作,避免了锁等同步机制的开销。而全局变量或静态变量则需要在多线程环境下进行同步操作,开销比较大。
2)在多线程环境中,使用线程局部存储可以避免竞争条件的出现,从而提高程序的并发能力。竞争条件是指多个线程同时对共享资源进行读写操作时可能会出现的不可预测的行为,如数据损坏、死锁等。线程局部存储使得每个线程都有自己的私有变量,避免了对共享资源的争用,从而避免了竞争条件的出现。
3)在某些情况下,线程局部存储还可以减少线程之间的上下文切换次数。当线程需要频繁地访问全局变量时,由于多个线程需要竞争同一个锁,导致线程之间频繁地切换上下文,降低了程序的性能。而使用线程局部存储可以将变量存储在每个线程的私有空间中,避免了线程之间的竞争,从而减少了上下文切换的次数。
3、那么既然是私有变量,不在线程之间共享,那我为什么不直接在线程里面定义一个私有变量存储?
》在多线程编程中,如果在线程函数内部直接定义一个私有变量,那么这个变量将会是在栈上分配的,每次调用线程函数时都会创建一个新的变量,而在线程函数返回后,这个变量将被销毁,这样就无法在多次调用线程函数时保持这个变量的状态。因此,线程局部存储提供了一种在线程之间共享变量的方法,同时又保证了每个线程都有自己的私有变量,可以在多次调用线程函数时保持变量的状态。
线程局部存储通过为每个线程创建一个唯一的指针来实现。线程在首次访问线程局部存储时,会根据这个指针来分配一个私有变量的存储空间,并将这个指针存储在当前线程的线程控制块(TCB)中。之后,每次访问线程局部存储时,都会通过这个指针来访问私有变量的存储空间。这样,每个线程都拥有自己的私有变量,并且可以在多次调用线程函数时保持变量的状态,同时又不会和其他线程的私有变量产生冲突。
二、TLS局部存储的应用场景
TLS变量主要应用于需要在线程之间保持状态的场景,这种状态可能是线程私有的,无法和其他线程共享。以下是一些适合使用TLS变量的场景:
-
)线程池中的任务信息。在线程池中,每个线程需要处理多个任务,需要保存当前正在处理的任务信息,可以使用TLS变量来保存每个线程的任务信息。
-
)线程特定的资源。例如,图形界面程序中,每个线程可能需要访问不同的窗口和控件,可以使用TLS变量来保存每个线程访问的窗口和控件信息。
-
)线程相关的日志信息。在多线程程序中,日志输出需要记录线程信息,可以使用TLS变量来保存每个线程的日志信息。
-
)线程局部的缓存。在一些高性能的应用程序中,为了避免频繁的内存分配和释放,可以使用线程局部的缓存来保存一些临时变量,可以使用TLS变量来实现。
总之,TLS变量适用于需要在线程之间保持状态的场景,而且这种状态需要是线程私有的,无法和其他线程共享。使用TLS变量可以避免使用锁等同步机制,从而提高程序的性能和并发能力。
不知道大家有没有理解,重点就是,在有些场景下,你不是需要一种生命周期比较长的变量吗?但是在C艹当中,生命周期比较长的,不随线程销毁的,不就是全局变量和静态变量static吗?事实上我们一般都使用static多一些,这是因为普通的全局变量被跨文件读写,有很大的安全性问题,而且很难排错。但是这种static不是可以被这个文件当中的所有线程/函数共享吗?这时候你是不是就需要加一些个同步机制来确保你属于你这个线程的变量不被别的线程修改,这样就需要一系列的PV操作,来加锁解锁,这就造成了很大的额外开销,因此程序猿们就想到了线程局部存储TLS这项技术,来去掉这些没必要的开销来实现线程的同步,当然如果你的程序设计本身就是大家一起去修改这个变量,就不要使用TLS了,就正常PV实现互斥同步就🆗了。
三、使用TLS变量的小案例
下面我们来简单写一段代码来测试一下TLS变量的用途:
这段代码里主要是设置一个银行账户初始化为500元,然后创建2个线程,使用事件Event控制他们先执行线程1,然后再执行线程2,线程1负责从银行账号里头取出来100元,然后执行打印操作,最后线程2再执行,打印原先的全局变量,结果发现还是500元没有发生变化。
#include <iostream>
#include<Windows.h>
HANDLE hEvent;
__declspec (thread) int g_account = 500;
void withdraw(int amount) {
g_account -= amount;
std::cout << "取出" << amount <<"元" << std::endl;
}
void print_account() {
std::cout << "Money left is:" << g_account << std::endl;
}
DWORD WINAPI thread_func1(LPVOID lpParam) {
std::cout << "***********************线程1执行了***********************" << std::endl;
withdraw(100);
print_account();
std::cout << "***********************线程1执行结束了***********************" << std::endl;
SetEvent(hEvent);
return 0;
}
DWORD WINAPI thread_func2(LPVOID lpParam) {
WaitForSingleObject(hEvent, INFINITE);
std::cout << "***********************线程2执行了***********************" << std::endl;
print_account();
std::cout << "***********************线程2执行结束了***********************" << std::endl;
return 0;
}
int main()
{
HANDLE hThread[2];
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
hThread[0] = CreateThread(NULL, 0, thread_func1, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, thread_func2, NULL, 0, NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hEvent);
std::cout << "对象已经全部销毁" << std::endl;
return 0;
}
执行结果:
四、TLS回调函数
下面我们进入本文的关键内容的学习:TLS回调函数,反调试,线程抢占执行
1、创建TLS回调函数步骤:
1)添加#pragma comment(linker,"/INCLUDE:__tls_used") 预编译指令,用于告诉链接器将TLS(线程本地存储)段包含在可执行文件或动态链接库(DLL)中。
#pragma comment(linker,"/INCLUDE:__tls_used")
2)注册TLS回调函数
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { tlsCallback,0 };
#pragma data_seg()
#pragma data_seg 是一个编译指令,用于指定数据段的名称。在 C++ 语言中,数据段是一块特殊的内存区域,用于存储程序中的全局变量和静态变量。
在 Windows 操作系统上,数据段还可以用于存储特殊类型的数据,例如 TLS(线程本地存储)回调函数。TLS回调函数是一种函数指针,当线程被创建或销毁时会被自动调用。TLS回调函数可以用于执行一些线程特定的初始化或清理工作。
3)实现TLS回调函数
void NTAPI tlsCallback(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
switch (Reason) {
case DLL_PROCESS_ATTACH:
std::cout << "进程创建了" << std::endl;
break;
case DLL_THREAD_ATTACH:
std::cout << "线程创建了" << std::endl;
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
}
以下是创建TLS回调函数的完整代码:
#include <iostream>
#include<Windows.h>
//1、添加预处理指令
#pragma comment(linker,"/INCLUDE:__tls_used")
HANDLE hEvent;
__declspec (thread) int g_account = 500;
//3、实现TLS回调函数
void NTAPI tlsCallback(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
switch (Reason) {
case DLL_PROCESS_ATTACH:
std::cout << "进程创建了" << std::endl;
break;
case DLL_THREAD_ATTACH:
std::cout << "线程创建了" << std::endl;
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
}
//2、注册TLS函数
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { tlsCallback,0 };
#pragma data_seg()
void withdraw(int amount) {
g_account -= amount;
std::cout << "取出" << amount <<"元" << std::endl;
}
void print_account() {
std::cout << "Money left is:" << g_account << std::endl;
}
DWORD WINAPI thread_func1(LPVOID lpParam) {
std::cout << "***********************线程1执行了***********************" << std::endl;
withdraw(100);
print_account();
std::cout << "***********************线程1执行结束了***********************" << std::endl;
SetEvent(hEvent);
return 0;
}
DWORD WINAPI thread_func2(LPVOID lpParam) {
WaitForSingleObject(hEvent, INFINITE);
std::cout << "***********************线程2执行了***********************" << std::endl;
print_account();
std::cout << "***********************线程2执行结束了***********************" << std::endl;
return 0;
}
int main()
{
HANDLE hThread[2];
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
hThread[0] = CreateThread(NULL, 0, thread_func1, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, thread_func2, NULL, 0, NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hEvent);
std::cout << "对象已经全部销毁" << std::endl;
return 0;
}
可以看到TLS运行在程序的最前面,于是我们可以利用这个特性来实现一个小功能
五、利用TLS回调函数实现反调试功能
1、原理部分
基于前面提到的特性,我们知道TLS回调的执行在OEP之前完成,它是由操作系统装载的时候检查TLS表时候进行调用的,而main函数创建主线程执行又是在OEP之后,所有我们可以利用这个特点来进行程序的反调试,达到抢占执行的目的。
我们先来分析一下TLS抢占执行的原理:
TLS(线程本地存储)回调函数是在程序启动时由操作系统调用的,这意味着它们可以在 main 函数执行之前执行。TLS 回调函数的执行顺序由操作系统决定,通常与 TLS 回调函数在程序中的定义顺序无关。
在 Windows 操作系统上,TLS 回调函数的执行顺序如下:
1)操作系统在加载可执行文件时,会查找文件中所有的 TLS 回调函数,并将它们按照优先级排序。优先级值越小的回调函数,优先级越高。注意:这里加载的时候就已经检查并执行了,不用从OEP进去后从主线程那边进去执行!
2)操作系统会在程序启动时(即调用 main 函数之前)调用这些 TLS 回调函数。调用顺序为优先级高的回调函数先调用,优先级低的回调函数后调用。
3)当线程被创建或销毁时,系统也会调用与 TLS 相关的回调函数。这些回调函数的调用顺序与程序启动时的回调函数调用顺序相同。
因此,TLS 回调函数可以在程序启动时执行一些初始化工作,例如初始化全局变量或资源。此外,它们也可以用于执行一些线程特定的初始化或清理工作,例如初始化线程局部变量或释放线程局部资源。
2、案例的具体实现
下面我们来具体实现一下这个小栗子,作用就是使用调试软件附加的时候,自动退出程序的目的,注意我们这里的测试软件使用的是win32dbg因为od里面实现了反反调试的手段,如果大家感兴趣后面可以安排一期来解决这个问题。
#include <iostream>
#include<Windows.h>
#pragma comment(linker,"/INCLUDE:__tls_used")
void NTAPI tlsCallback(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
if (Reason == DLL_PROCESS_ATTACH) {
BOOL result = false;
HANDLE hNewHandle = 0;
DuplicateHandle(
GetCurrentProcess(),
GetCurrentProcess(),
GetCurrentProcess(),
&hNewHandle,
NULL, NULL,
DUPLICATE_SAME_ACCESS
);
CheckRemoteDebuggerPresent(hNewHandle, &result);
if (result) {
MessageBoxA(0, "程序被调试了!", "警告", MB_OK);
ExitProcess(0);
}
}
}
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { tlsCallback,0 };
#pragma data_seg()
int main() {
printf("main函数执行了\n");
}
这段代码的核心就是通过TLS回调函数的优先执行来执行我们的反调试代码,在反调试代码当中通过CheckRemoteDebuggerPresent(hNewHandle, &result)函数来检测hNewHandle对应的进程有没有被附加。这里的hNewHandle是通过DuplicateHandle()函数将原始句柄所对应的内核对象复制一份到当前进程的内核对象表中,并返回一个新的句柄,这个新的句柄与原始句柄所对应的内核对象是一模一样的,包括对象的属性、访问权限等信息。
那么为什么要费劲复制一个新的句柄呢?
》DuplicateHandle 函数复制的新的句柄与原始句柄是两个不同的对象,它们具有不同的内存地址和句柄值。这意味着,对新的句柄的操作不会影响原始句柄,而对原始句柄的操作也不会影响新的句柄。在这种情况下,即使恶意软件在外部修改了原始句柄的属性或者访问权限,也不会影响到新的句柄,从而保证了检测调试器的准确性。
补充:DuplicateHandle 函数用于复制一个句柄,它的参数含义如下:
hSourceProcessHandle:需要复制句柄所在的进程句柄。可以是本地进程句柄,也可以是其他进程的进程句柄。
hSourceHandle:需要复制的句柄。可以是任何类型的内核对象句柄,如文件句柄、事件句柄等。
hTargetProcessHandle:复制后的句柄所属的进程句柄。通常为本地进程句柄。
lpTargetHandle:指向新句柄的指针。该参数接收函数创建的新句柄。
dwDesiredAccess:指定新句柄的访问权限。可以与原句柄的访问权限不同。
bInheritHandle:指定新句柄是否可以被子进程继承。如果为 TRUE,则子进程可以继承新句柄;如果为 FALSE,则子进程不能继承新句柄。
dwOptions:指定复制句柄的选项。常用的选项包括:
DUPLICATE_CLOSE_SOURCE:复制完成后关闭源句柄。
DUPLICATE_SAME_ACCESS:新句柄与原句柄具有相同的访问权限。
lpTargetHandle:接收函数返回值,如果函数执行成功,则它会返回一个指向新句柄的指针。
执行结果:
点击确定之后:
注意这里必须使用x32dbg,因为其他的调试软件一般都有反反调试的功能!