While 奥菲克·希隆代码中缺少一种成分是正确的,他的答案只包含整个成分的一部分。可以找到完整的工作解决方案here这又取自here.
有关其工作原理的解释,您可以参考这个博客(假设我们正在使用 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;
}
EDIT:
肯定需要一些解释,所以让我们看看代码中发生了什么。
-
如果我们想使用 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
};
此代码初始化值IMAGE_TLS_DIRECTORY(64)
TLS 目录条目指向的结构。回调数组的指针是它的字段之一。操作系统加载程序遍历该数组,并调用指向的函数,直到遇到空指针。
有关 PE 文件中目录的信息,请参阅此链接来自 MSDN并搜索描述IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
.
x86注意:如你所见,同名_tls_used
满足于tlssup.c
适用于 x86 和 x64 平台,但额外_
在 x86 构建中包含此名称时添加。这不是拼写错误,而是链接器功能,因此有效命名__tls_used
被采取。
- 现在我们正在创建回调。其类型可以从定义中获得
IMAGE_TLS_DIRECTORY(64)
可以在以下位置找到:winnt.h
,有一个字段
for 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 回调数组的范围_tls_used
, 它
是表格某个部分的必要位置“.CRT$XLx“
.
实际上可能有 2 个以上的回调(我们实际上只使用一个),我们可能想按顺序调用它们,现在我们知道如何做了。只需将这些回调放在按字母顺序命名的部分中即可。
EXTERN_C
添加以禁止 C++ 风格的名称修改并使用 C 风格的名称修改。
const
and const_seg
用于 x64 版本的代码,否则它将无法工作,我不知道确切的原因,猜测可能是 x86 和 x64 平台的 CRT 部分的访问权限不同。
最后,我们要包含回调函数的名称,以便链接器知道它将添加到 TLS 回调数组中。有关附加的说明_
对于 x64 版本,请参阅上面第 1 页的末尾。