关于窗口中的 TLS 回调
about TLS Callback in windows
这是测试代码
#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;
}
编辑:
肯定需要一些解释,所以让我们看看代码中发生了什么。
如果我们想使用 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
- 对于VS 12.0,默认情况下它位于:
它按以下方式定义:
#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 和 x64 平台在 tlssup.c
中都满足相同的名称_tls_used
,但在为 x86 build 包含此名称时会添加额外的_
。这不是错别字,而是链接器功能,因此可以有效地命名__tls_used
。
- 现在我们正处于创建回调的阶段。它的类型可以从
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
相同,它可以处理同一组事件:进程\线程附加\分离。
- 是时候注册回调了。首先看一下
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 样式的名称。
const
和const_seg
用于x64版本的代码,因为否则它无法工作,我不知道确切的原因,猜测可能是CRT部分的访问权限对于x86和x64平台是不同的。
最后,我们将包含回调函数的名称,以便链接器知道它将被添加到TLS回调数组中。有关 x64 构建的其他_
的说明,请参阅上面的第 1 页末尾。
还必须显式添加符号__tls_used。有了这个,你的代码应该可以工作:
#pragma comment(linker,"/include:__tls_used")
- 架构决策:返回std::future还是提供回调
- 正在为Xtensa simcall函数编写回调函数
- 如何在C++中使用非静态成员函数作为回调函数
- FLTK:按下哪个按钮 - 将数字传递给按钮的回调 (lambda)
- 在简单示例中,Python3 + ctypes 回调会导致内存泄漏
- 用于在回调中调用解析器的设计模式
- 如何使用C++对象的成员函数作为 C 样式回调?
- Java从C++回调到C++回调
- 如何将成员函数作为回调参数传递给需要"typedef-ed"自由函数指针的函数?
- 从不同的 cpp 调用回调函数会导致bad_function_call
- pcap_handler回调仅在使用 NPCAP v0.9991 时包含空数据包
- 不带轮询的 SDL2 事件回调
- C++存储带有可变参数的回调
- 如何使用 Node-addon-API 实现 node-nan 回调
- 处理影响跨不同线程共享对象的定时回调的最佳方法是什么?
- 访问类C++ C 样式回调
- 处理类内的回调时,必须调用对非静态成员函数的引用
- 在窗口执行目标 TLS 回调之前注入 dll
- 从TLS回调中获取线程信息
- 关于窗口中的 TLS 回调