关于窗口中的 TLS 回调

about TLS Callback in windows

本文关键字:TLS 回调 窗口 于窗口      更新时间:2023-10-16

这是测试代码

#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) 生成运行结果:主线程 TLS 值 = 1

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

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

为什么?

虽然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;
}

编辑:

肯定需要一些解释,所以让我们看看代码中发生了什么。

  1. 如果我们想使用 TLS 回调,那么我们要明确地告诉编译器。它是通过包含变量_tls_used来完成的,该变量具有指向回调数组(以 null 结尾)的指针。对于此变量的类型,您可以在 Visual Studio 附带的 CRT 源代码中查阅tlssup.c

    • 对于VS 12.0,默认情况下它位于: c:Program Files (x86)Microsoft Visual Studio 12.0VCcrtsrc
    • 对于VS 14.0,默认情况下它位于:c:Program Files (x86)Microsoft Visual Studio 14.0VCcrtsrcvcruntime

它按以下方式定义:

#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)结构的值。指向回调数组的指针是它的字段之一。此数组由操作系统加载程序遍历,并调用指向函数,直到满足空指针。

有关 PE 文件中目录的信息,请参阅 MSDN 中的此链接并搜索 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] 的说明。

x86

注意:如您所见,x86 和 x64 平台在 tlssup.c 中都满足相同的名称_tls_used,但在为 x86 build 包含此名称时会添加额外的_。这不是错别字,而是链接器功能,因此可以有效地命名__tls_used

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

对于 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相同,它可以处理同一组事件:进程\线程附加\分离。

  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 部分时,了解$中有什么特别之处非常重要,因此引用了名为"隐式 TLS 的编译器和链接器支持"的文章:

PE 映像中的非标头数据被放置在一个或多个部分中, 哪些是具有一组通用属性的内存区域(例如 页面保护)。__declspec(allocate(“section-name”))关键字 (特定于 CL)告诉编译器特定变量是 放置在最终可执行文件的特定部分中。编译器 此外,还支持连接名称相似的部分 分成一个更大的部分。此支持通过前缀 带有$字符的节名称,后跟任何其他文本。这 编译器将生成的节与 同名,在$字符(含)处截断。

编译器在以下情况下按字母顺序对各个部分进行排序 连接它们(由于在本节中使用了 $ 字符 名称)。这意味着在内存中(在最终可执行映像中),一个 “.CRT$XLB”部分中的变量将在 “.CRT$XLA”节,但在“.CRT$XLZ”节中的变量之前。该 C 运行时使用编译器的这种怪癖来创建 null 数组 指向 TLS 回调的终止函数指针(存储指针) 在“.CRT$XLZ”部分中是空终止符)。因此,为了 确保声明的函数指针驻留在 _tls_used引用的 TLS 回调数组的限制,它 是表格“.CRT$XLx“部分中的必要位置。

实际上可能有 2+ 个回调(我们实际上只会使用一个),我们可能想按顺序调用它们,现在我们知道怎么做了。只需将这些回调放在按字母顺序命名的部分中即可。

添加EXTERN_C以禁止C++样式的名称重整并使用 C 样式的名称。

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

最后,我们将包含回调函数的名称,以便链接器知道它将被添加到TLS回调数组中。有关 x64 构建的其他_的说明,请参阅上面的第 1 页末尾。

还必须显式添加符号__tls_used。有了这个,你的代码应该可以工作:

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