关于windows中的TLS回调
Posted
技术标签:
【中文标题】关于windows中的TLS回调【英文标题】:about TLS Callback in windows 【发布时间】:2013-01-26 15:03:26 【问题描述】:这是测试代码
#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 value = 1
使用多线程调试 (/MTd) 构建 运行结果:主线程 tls value = 0
貌似不能捕获使用MTd时创建的主线程
为什么?
【问题讨论】:
您使用未记录的 CRT 实施细节使保修失效。 “为什么”是 .CRT$XL?部分仅用于在程序链接 CRT 的 DLL 版本时初始化 TLS。它不与 /MT 一起使用。 感谢 Hans Passant 以及如何使用 /MT 进行 TLS 回调 你不能,这只是为了 CRT 的好处,它不是语言标准的一部分。 @HansPassant 虽然您在技术上是正确的,但此代码只是缺少一个成分。请参阅下面的答案。 【参考方案1】:虽然 Ofek Shilon 认为代码缺少一种成分是正确的,但他的回答只包含整个成分的一部分。完整的工作解决方案可以在 here 找到,它又取自 here。
有关其工作原理的说明,您可以参考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;
编辑:
肯定需要一些解释,所以让我们看看代码中发生了什么。
如果我们想使用 TLS 回调,那么我们要明确地告诉编译器。它是通过包含变量_tls_used
来完成的,该变量具有指向回调数组(以空结尾)的指针。对于此变量的类型,您可以在 Visual Studio 随附的 CRT 源代码中查阅 tlssup.c
:
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 注意:如您所见,在 x86 和 x64 平台的 tlssup.c
中遇到了相同的名称 _tls_used
,但在为 x86 构建包含此名称时会添加额外的 _
。这不是拼写错误,而是链接器功能,因此有效地命名为 __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
相同,它可以处理相同的事件集:process\thread attach\detach。
-
是时候注册回调了。
首先看一下来自
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 运行时使用编译器的这个怪癖来创建一个 null 数组 终止的指向 TLS 回调的函数指针(存储的指针 在“.CRT$XLZ”
部分中是空终止符)。因此,为了 确保声明的函数指针位于_tls_used
引用的 TLS 回调数组的范围,它 必须放在“.CRT$XLx“
形式的部分中。
实际上可能有 2 个以上的回调(我们实际上只会使用一个),我们可能希望按顺序调用它们,现在我们知道如何调用了。只需将这些回调放在按字母顺序命名的部分中即可。
EXTERN_C
被添加以禁止 C++ 样式的名称重整并使用 C 样式的名称。
const
和 const_seg
用于 x64 版本的代码,否则无法工作,我不知道确切原因,猜测可能是 x86 和 CRT 部分的访问权限不同x64 平台。
最后,我们要包含回调函数的名称,以便链接器知道它将被添加到 TLS 回调数组中。有关 x64 构建的额外 _
的注意事项,请参阅上面第 1 页的末尾。
【讨论】:
虽然这段代码可以回答问题,但最好包含一些上下文,解释如何它的工作原理和何时 i> 使用它。从长远来看,纯代码答案没有用处。由于链接失效,其他地方的解释可能会消失。 另请注意,依赖编译器内部意味着当您升级到较新版本的编译器时它可能会停止工作。 添加了解释,但仍然缺少一些细节:1)const_seg
for x64 而不是data_seg
; 2) 在为 x86 命名时附加 _
。谁能告诉我这些特性?【参考方案2】:
您还必须显式添加符号 __tls_used。有了这个你的代码应该可以工作:
#pragma comment(linker,"/include:__tls_used")
【讨论】:
以上是关于关于windows中的TLS回调的主要内容,如果未能解决你的问题,请参考以下文章