c++ Windows中的TLS回调

mjqavswn  于 2023-11-19  发布在  Windows
关注(0)|答案(3)|浏览(123)

下面是测试代码:

#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;
}

字符串
使用多线程DLL(/MDd)生成运行结果:main thread tls value = 1
使用多线程编译器(/MTd)生成运行结果:main thread tls value = 0
看起来我无法捕获使用MTd时创建的主线程。
为什么会这样呢?

z31licg0

z31licg01#

虽然Ofek Shilon是正确的,代码缺少一个成分,但他的答案只包含整个成分的一部分。
有关它如何工作的解释,您可以参考this blog(假设我们使用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;
}

字符串

编辑:

肯定需要一些解释,所以让我们看看代码中发生了什么。
1.如果我们想使用TLS回调,那么我们要明确地告诉编译器。这是通过包含变量_tls_used来完成的,变量_tls_used有一个指向回调数组的指针(null终止)。对于这个变量的类型,你可以在CRT源代码中查阅tlssup.c,它是沿着Visual Studio一起提供的:

  • 对于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
};


这段代码为TLS目录条目指向的IMAGE_TLS_DIRECTORY(64)结构提供了值。回调数组的指针是它的字段之一。这个数组被OS加载器遍历,并被调用,直到遇到空指针。

  • 有关PE文件中目录的信息,请参阅this link from MSDN并搜索IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]的描述。*
  • x86注意:正如你所看到的,_tls_used在x86和x64平台的tlssup.c中也有相同的名称,但是在x86版本中包含这个名称时会添加额外的_。这不是一个错别字,而是链接器特性,所以有效地命名为__tls_used。*

1.现在我们开始创建回调函数了。它的类型可以从winnt.h中的IMAGE_TLS_DIRECTORY(64)定义中获得,其中有一个字段
对于x64:

ULONGLONG AddressOfCallBacks;  // PIMAGE_TLS_CALLBACK *;


对于x86:

DWORD   AddressOfCallBacks;  // PIMAGE_TLS_CALLBACK *


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

typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (PVOID DllHandle, DWORD Reason, PVOID Reserved);


它与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;


在命名PE部分时,了解$的特殊之处非常重要,因此引用了命名为"Compiler and linker support for implicit TLS"的文章:
PE图像中的非标题数据被放置到一个或多个部分中,它们是具有一组公共属性的内存区域(如页面保护)。__declspec(allocate(“section-name”))关键字(CL特定的)告诉编译器一个特定的变量将被放置在最终可执行文件的特定部分。编译器还支持类似的连接-命名的节合并成一个更大的节。这种支持是通过在节名前面加上$字符,后跟任何其他文本来激活的。编译器将得到的节与同名的节连接起来,在$字符处截断(包括)。
编译器在连接各个节时按顺序排列各个节(由于在节名中使用了$字符)。这意味着在内存中(在最终的可执行映像中),“.CRT$XLB”部分中的变量将在“.CRT$XLA”部分中的变量之后,但在“.CRT$XLZ”部分中的变量之前。C运行时使用编译器的这种怪癖来创建一个指向TLS回调的null终止函数指针数组(存储在“.CRT$XLZ”部分的指针是null终止符)。因此,为了确保声明的函数指针驻留在_tls_used引用的TLS回调数组的范围内,必须将其放置在“.CRT$XLx“形式的部分中。
实际上可能有2个以上的回调函数(我们实际上只使用一个),我们可能想按顺序调用它们,现在我们知道如何调用了。只需将这些回调函数按字母顺序命名。
添加EXTERN_C以禁止C++风格的名称mangling,并使用C风格的名称mangling。
constconst_seg用于x64版本的代码,因为否则它无法工作,我不知道确切的原因,猜测可能是CRT部分的访问权限对于x86和x64平台不同。
最后,我们要包括回调函数的名称,以便链接器知道它将被添加到TLS回调数组中。有关x64构建的额外_的注意事项,请参阅上文第1页的末尾。

6bc51xsx

6bc51xsx2#

您还必须显式添加符号__tls_used。使用此符号,您的代码应该可以工作:

#pragma comment(linker,"/include:__tls_used")

字符串

yduiuuwa

yduiuuwa3#

如果使用隐式TLS变量(__declspec(thread)),则不需要使用#pragma comment(linker,"/include:__tls_used")

相关问题