线程局部存储(TLS)

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";

在这里插入图片描述

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值