关于Windows中的TLS回调

chi*_*yer 6 c++ thread-local-storage

这是测试代码

#include "windows.h"
#include "iostream"
using namespace std;

__declspec(thread) int tls_int = 0;

void NTAPI tls_callback(PVOID, DWORD dwReason, PVOID)   
{
    tls_int = 1;
}

#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
#pragma data_seg()

int main()
{
    cout<<"main thread tls value = "<<tls_int<<endl;

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

使用多线程调试DLL(/ MDd)运行结果:主线程tls值= 1

使用多线程调试(/ MTd)运行结果:主线程tls值= 0

看起来无法捕获使用MTd时创建的主线程

为什么?

gre*_*ece 8

虽然Ofek Shilon对代码缺少一种成分是正确的,但他的回答只包含整个成分的一部分.这里可以找到完整的工作解决方案,而这里又是从这里开始的.

有关它如何工作的解释,您可以参考此博客(假设我们正在使用VC++编译器).

为方便起见,代码发布在下面(请注意,支持x86和x64平台):

#include <windows.h>

// Explained in p. 2 below
void NTAPI tls_callback(PVOID DllHandle, DWORD dwReason, PVOID)
{
    if (dwReason == DLL_THREAD_ATTACH)
    {
        MessageBox(0, L"DLL_THREAD_ATTACH", L"DLL_THREAD_ATTACH", 0);
    }

    if (dwReason == DLL_PROCESS_ATTACH)
    {
        MessageBox(0, L"DLL_PROCESS_ATTACH", L"DLL_PROCESS_ATTACH", 0);
    }
}

#ifdef _WIN64
     #pragma comment (linker, "/INCLUDE:_tls_used")  // See p. 1 below
     #pragma comment (linker, "/INCLUDE:tls_callback_func")  // See p. 3 below
#else
     #pragma comment (linker, "/INCLUDE:__tls_used")  // See p. 1 below
     #pragma comment (linker, "/INCLUDE:_tls_callback_func")  // See p. 3 below
#endif

// Explained in p. 3 below
#ifdef _WIN64
    #pragma const_seg(".CRT$XLF")
    EXTERN_C const
#else
    #pragma data_seg(".CRT$XLF")
    EXTERN_C
#endif
PIMAGE_TLS_CALLBACK tls_callback_func = tls_callback;
#ifdef _WIN64
    #pragma const_seg()
#else
    #pragma data_seg()
#endif //_WIN64

DWORD WINAPI ThreadProc(CONST LPVOID lpParam) 
{
    ExitThread(0);
}

int main(void)
{
    MessageBox(0, L"hello from main", L"main", 0);
    CreateThread(NULL, 0, &ThreadProc, 0, 0, NULL);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

编辑:

绝对需要一些解释,所以让我们来看看代码中发生了什么.

  1. 如果我们想要使用TLS回调,那么我们将明确告诉编译器.它由变量的包含完成,该变量_tls_used具有指向回调数组的指针(以null结尾).对于此变量的类型,您可以参考tlssup.c随Visual Studio一起提供的CRT源代码:

    • 对于VS 12.0,默认情况下它位于: c:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\crt\src\
    • 对于VS 14.0,默认情况下它位于: c:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src\vcruntime\

它按以下方式定义:

#ifdef _WIN64

_CRTALLOC(".rdata$T") const IMAGE_TLS_DIRECTORY64 _tls_used =
{
        (ULONGLONG) &_tls_start,        // start of tls data
        (ULONGLONG) &_tls_end,          // end of tls data
        (ULONGLONG) &_tls_index,        // address of tls_index
        (ULONGLONG) (&__xl_a+1),        // pointer to call back array
        (ULONG) 0,                      // size of tls zero fill
        (ULONG) 0                       // characteristics
};

#else  /* _WIN64 */

_CRTALLOC(".rdata$T")
const IMAGE_TLS_DIRECTORY _tls_used =
{
        (ULONG)(ULONG_PTR) &_tls_start, // start of tls data
        (ULONG)(ULONG_PTR) &_tls_end,   // end of tls data
        (ULONG)(ULONG_PTR) &_tls_index, // address of tls_index
        (ULONG)(ULONG_PTR) (&__xl_a+1), // pointer to call back array
        (ULONG) 0,                      // size of tls zero fill
        (ULONG) 0                       // characteristics
};
Run Code Online (Sandbox Code Playgroud)

此代码初始化IMAGE_TLS_DIRECTORY(64)TLS目录条目指向的结构的值.指向回调数组的指针是其中的一个字段.OS加载程序遍历此数组,并且在满足空指针之前调用指向的函数.

有关PE文件中目录的信息,请参阅MSDN中的此链接并搜索其描述IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES].

x86注意:正如您所看到的,x86和x64平台都_tls_used满足相同的名称tlssup.c,但_在为x86 build包含此名称时会添加其他名称.它不是拼写错误而是链接器功能,因此可以有效地命名__tls_used.

  1. 现在我们正在创建回调.它的类型可以从其定义中IMAGE_TLS_DIRECTORY(64)找到winnt.h,有一个字段

对于x64:

ULONGLONG AddressOfCallBacks;  // PIMAGE_TLS_CALLBACK *;
Run Code Online (Sandbox Code Playgroud)

对于x86:

DWORD   AddressOfCallBacks;  // PIMAGE_TLS_CALLBACK *
Run Code Online (Sandbox Code Playgroud)

回调的类型定义如下(也来自winnt.h):

typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (PVOID DllHandle, DWORD Reason, PVOID Reserved);
Run Code Online (Sandbox Code Playgroud)

它与for相同DllMain,它可以处理同一组事件:process\thread attach\detach.

  1. 是时候注册回调了.首先来看看以下代码tlssup.c:

分配的部分:

_CRTALLOC(".CRT$XLA") PIMAGE_TLS_CALLBACK __xl_a = 0;

/* NULL terminator for TLS callback array.  This symbol, __xl_z, is never
 * actually referenced anywhere, but it must remain.  The OS loader code
 * walks the TLS callback array until it finds a NULL pointer, so this makes
 * sure the array is properly terminated.
 */

_CRTALLOC(".CRT$XLZ") PIMAGE_TLS_CALLBACK __xl_z = 0;
Run Code Online (Sandbox Code Playgroud)

$命名PE部分时,了解什么是特殊的非常重要,因此引用文章名为"隐式TLS的编译器和链接器支持":

PE映像中的非标头数据被放置在一个或多个部分中,这些部分是具有共同属性集(例如页面保护)的存储器区域.的__declspec(allocate(“section-name”))关键字(CL-特异性)告诉编译器,特定变量是被放置在一个特定的部分中的最后的可执行文件.编译器还支持将类似命名的节连接成一个更大的节.通过在节名称前加上$字符后跟任何其他文本来激活此支持.编译器将结果部分与同名部分连接起来,在$字符(包括)处截断.

连接时,编译器按字母顺序对各个部分进行排序(由于在部分名称中使用了$字符).这意味着在内存中(在最终的可执行映像中),该“.CRT$XLB”部分中的变量将在该“.CRT$XLA”部分中的变量之后 但在“.CRT$XLZ”部分中的变量之前.C运行时使用编译器的这个怪癖来创建一个到TLS回调的空终止函数指针数组(指针存储在该“.CRT$XLZ”部分中作为空终止符).因此,为了确保声明的函数指针位于被引用的TLS回调数组的范围内,_tls_used必须放在表单的一部分中“.CRT$XLx“.

实际上可能有2个以上的回调(我们实际上只使用一个),我们可能想按顺序调用它们,现在我们知道如何.只需将这些回调放在按字母顺序命名的部分中.

EXTERN_C 添加到禁止C++风格的名称修改并使用C风格的名称.

const并且const_seg用于x64版本的代码,因为否则它无法工作,我不知道确切的原因,猜测可能是CRT部分的访问权限对于x86和x64平台是不同的.

最后,我们要为链接器包含回调函数的名称,以便知道它将被添加到TLS回调数组中.有关_x64 build的附加说明,请参阅上面的第1页末尾.


Ofe*_*lon 1

您还必须显式添加符号 __tls_used。这样你的代码应该可以工作:

#pragma comment(linker,"/include:__tls_used")
Run Code Online (Sandbox Code Playgroud)