TLS
TLS:Thread Local Storage,线程局部存储,是一种存储变量的方法。
这个变量在它所在的线程内是全局访问的,但是不能被其他线程访问,因而实现了变量的线程独立性。
TLS的分类
根据 线程局部存储的数据所用空间在程序运行期,操作系统完成的是动态申请还是静态分配,TLS技术分为两种:
- 动态TLS:通过四个Win32 API实现对线程局部数据的存储;
- 静态TLS:通过预先在PE文件中声明数据存储空间,由PE加载器加载该PE进入内存后为其预留存储空间实现。
动态TLS
动态TLS适用于DLL,因为对于DLL来说,无法确定加载这个DLL的宿主程序里有多少个线程,每个线程的数据又是如何定义的。
动态TLS存在四个API函数(位于kernel32.dll中):
- TlsAlloc:分配一个TLS索引
- TlsGetValue:根据索引从TLS槽中取值
- TlsSetValue:存储指定值到指定索引处
- TlsFree:释放TLS索引
应用程序在合适的时候会调用这四个函数,通过TLS索引对进程中每个线程的存储区进行统一操作。
**代码示例:**创建4个线程,弹窗显示各线程执行所用的时间
#include <Windows.h>
#include <iostream>
#define THREAD_COUNT 4
DWORD TlsIndex = 0;
void InitTime()
{
DWORD Start = GetTickCount();
BOOL IsOk = TlsSetValue(TlsIndex, (LPVOID)(DWORD_PTR)Start);
if (!IsOk)
{
MessageBoxW(NULL, L"TlsSetValue Failed()", L"Error", NULL);
}
}
DWORD GetLostTime()
{
DWORD Temp = GetTickCount();
PVOID v1 = TlsGetValue(TlsIndex);
if (v1 == NULL)
{
MessageBoxW(NULL, L"TlsGetValue Failed()", L"Error", NULL);
}
return Temp - (DWORD)v1;
}
UINT32
random(UINT32 m, UINT32 n)
{
return rand() % (n - m + 1) + m;
}
DWORD ThreadProcedure(LPVOID ParameterData)
{
InitTime();
Sleep(random(1000, 2000));
WCHAR Buffer[MAX_PATH] = { 0 };
swprintf_s(Buffer, L"线程%d结束,用时%d毫秒", GetCurrentThreadId(), GetLostTime());
MessageBoxW(NULL, Buffer, L"Hint", NULL);
return 0;
}
void main(int argc, char* argv[])
{
// 分配一个TLS索引,该进程的任何线程都可以用该索引来存储和检取线程中的值
TlsIndex = TlsAlloc();
for (DWORD i = 0; i < THREAD_COUNT; i++)
{
DWORD ThreadID = 0;
HANDLE ThreadHandle = CreateThread(
NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProcedure, NULL, 0, &ThreadID);
WaitForSingleObject(ThreadHandle, INFINITE);
CloseHandle(ThreadHandle);
}
TlsFree(TlsIndex);
getchar();
}
运行结果:这里只列出两个
静态TLS
静态TLS会预先将变量定义在PE文件内部,一般是在.tls
节。
定义静态TLS变量,只需在程序中声明一下:
_declspec(thread) int tlsflag = 1; // _declspec(thread)前缀是Microsoft为Visual C++编译器增加的一个修饰符
当编译器对程序进行编译的时候,会将所有TLS变量放到它们自己的.tls
段中,链接器会将所有对象模块中的.tls
段合并成一个大的并保存到PE文件中,也就是.tls
节。链接器会设置TLS目录中的AddressOfIndex字段,保存TLS索引。
注意:
通过静态TLS只能用于静态加载的映像文件;在DLL中用静态TLS数据并不可靠,因为很难确定这个DLL以及静态链接到这个DLL的其他DLL不会被动态加载。
TLS目录
PE文件数据目录中有一项专门用来描述TLS目录。
#define IMAGE_DIRECTORY_ENTRY_TLS 9
// TLS模板是一块数据,用于对TLS数据进行初始化。每当创建线程时,系统都要复制所有这些数据。它的StartAddress,是一个VA。
// 所以在.reloc节中应当有一个相应的基址重定位信息用来说明这个地址。
typedef struct _IMAGE_TLS_DIRECTORY32 {
ULONG StartAddressOfRawData; // TLS模板的起始地址
ULONG EndAddressOfRawData; // TLS模板的结束地址,最后一个字节的地址,不包括用于填充的0
ULONG AddressOfIndex; // TLS索引的位置,索引的具体值由加载器确定。这个位置在.data中。
ULONG AddressOfCallBacks; // 指向TLS回调函数的指针数组,数组以NULL结尾。若没有回调函数,则指向的位置是4个字节的0
ULONG SizeOfZeroFill; // 用0填充的字节数
ULONG Characteristics; // 保留
} IMAGE_TLS_DIRECTORY32, *PIMAGE_TLS_DIRECTORY32;
typedef struct _IMAGE_TLS_DIRECTORY64 {
ULONGLONG StartAddressOfRawData;
ULONGLONG EndAddressOfRawData;
ULONGLONG AddressOfIndex;
ULONGLONG AddressOfCallBacks;
ULONG SizeOfZeroFill;
ULONG Characteristics;
} IMAGE_TLS_DIRECTORY64, *PIMAGE_TLS_DIRECTORY64;
TLS回调函数
程序可以提供一个或多个TLS回调函数,来支持对TLS数据进行附加的初始化和终止操作。
创建或终止线程时,都会自动调用TLS回调函数。且TLS回调函数的调用运行要先于EP代码的执行,这使得它可以作为一种反调试技术来使用。
回调函数的原型:
typedef void (NTAPI *PIMAGE_TLS_CALLBACK)(
PVOID DllHandle, // DLL句柄
DWORD Reason, // DLL_PROCESS_ATTACH/DLL_PROCESS_DETACH/DLL_THREAD_ATTACH/DLL_THREAD_DETACH
PVOID Reserved ); // 预留,为0
示例代码:
#include <Windows.h>
#include <iostream>
#pragma comment(linker, "/INCLUDE:__tls_used")
_declspec(thread) int test = 26214; //0x6666
_declspec(thread) int test1 = 34952; //0x8888
_declspec(thread) PCHAR test2 = "HelloWorld";
void TLS_CALLBACK1(PVOID DllHandle,
DWORD Reason,
PVOID Reserved)
{
printf("TlsCallback1(), Reason: %d\r\n", Reason);
}
void TLS_CALLBACK2(PVOID DllHandle,
DWORD Reason,
PVOID Reserved)
{
printf("TlsCallback2(), Reason: %d\r\n", Reason);
}
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK Callbacks[] = {
(PIMAGE_TLS_CALLBACK)TLS_CALLBACK1,
(PIMAGE_TLS_CALLBACK)TLS_CALLBACK2,
0
};
#pragma data_seg()
DWORD ThreadProcedure(LPVOID ParameterData)
{
printf("ThreadProcedure()\r\n");
return 0;
}
int main(int argc, char* argv[])
{
printf("main()\r\n");
HANDLE ThreadHandle = CreateThread(
NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProcedure, NULL, 0, NULL);
WaitForSingleObject(ThreadHandle, INFINITE);
CloseHandle(ThreadHandle);
return 0;
}
程序运行结果为:
可见,在主线程和新线程的EP前后,都会调用Tls回调函数。其实正常的话,在最后还会有一对Reason是0的调用,这里没有我也很纳闷,可能是因为没来得及打印程序就退出了?
除此之外,用PEview看一下生成的exe文件,发现.tls节中存储了变量的初始值。
_declspec(thread) int test = 26214; //0x6666
_declspec(thread) int test1 = 34952; //0x8888
_declspec(thread) PCHAR test2 = "HelloWorld";