线程局部存储,Part 3:编译器和链接器对隐式TLS的支持

来源:互联网 发布:php api接口开发文档 编辑:程序博客网 时间:2024/06/04 01:10

原文网址:http://www.nynaeve.net/?p=183

线程局部存储,Part 3:编译器和链接器对隐式TLS的支持

上次,我们探讨了显式TLS操作所采用的机制(包括TlsGetValue、TlsSetValue和其它相关例程)。

尽管显式TLS被大量使用,但是TLS机制的更有意思的部分却是加载器对隐式TLS的支持或是编译器中的__declspec(thread)变量。虽然两种TLS机制设计用于实现相似的功能,即提供线程相关的数据存储,但是它们的实现具有非常大的差别。

当你使用__declspec(thread)扩展存储类声明一个变量时,编译器和链接器合作为变量在映像文件的一个特殊区域(一个特殊段)中分配存储空间。通常,所有__declspec(thread)存储类变量被放置在PE文件的.tls节中,技术上来说不用非得这样(事实上,线程局部变量都不用放在自己单独的节内,从链接器的角度来看,仅要求把它们放在连续的空间上即可)。在硬盘上,某一PE文件中,这块空间包含所有线程局部变量的初始化数据。这块数据区永远不会被修改或是被线程局部变量引用;这里的数据仅仅用于在线程刚被创建时用于初始化为线程局部变量新分配的内存空间。

编译器和链接器使用几个特殊的变量来支持隐式TLS。具体来说,变量_tls_used(变量类型为IMAGE_TLS_DIRECTORY)由C运行时库创建,静态链接时该变量表示TLS目录结构并被最终的映像文件使用(由于名字修饰的原因,在C++中需要使用extern “C”链接,存储类型为外部引入,因为CRT代码已经创建了该变量)。TLS目录是PE文件头的一部分,用于告诉加载器如何管理线程局部变量,链接时,链接器查找变量_tls_used,并确保其与最终PE文件中的TLS目录重叠。(这里不太确定是什么意思)

C运行时库中声明变量_tls_used的源代码位于tlssup.c文件中(与Visual Studio一起发布)。_tls_used标准的声明方式如下所示:

_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

};

同样,CRT代码提供了一种机制,该机制允许程序注册一系列与DllMain具有类似签名的TLS回调函数。(这些回调函数可以在主映像文件中存在,而DllMain则不可以)。回调函数类型为PIMAGE_TLS_CALLBACK,TLS目录指向一个以NULL结尾的callbacks数组(这些函数将按顺序调用)。

对于一般的PE文件不会使用TLS回调(实际中,大部分使用DllMain来完成独立于线程的初始化工作)。但是TLS回调支持却是完全可以工作的。为了使用CRT提供的TLS回调支持,需要我们声明一个存放在以“.CRT$XLx“为名的节里面,这里x是一个位于A和Z之间的字母。例如,如下的代码片段:

#pragma section(“.CRT$XLY”,long,read)

extern “C” __declspec(allocate(“.CRT$XLY”))

PIMAGE_TLS_CALLBACK _xl_y = MyTlsCallback;

需要如此奇怪的节名是因为TLS回调指针需要进行内存排序的原因。为了理解这种特殊声明的作用,需要首先明白编译器和链接器是如何组织PE文件中的数据的。

PE文件中,除了头部数据,其它均是分不同节存储的,节就是具有相同属性(也保护属性)集合的内存区域。关键字__declspec(allocate(“section-name”))告诉编译器(这里应该是链接器,原文有错,下同,但仍然按原文翻译)在最终PE文件中其作用域内的内容放在指定的节内。编译器额外支持将相似名字的节合并为一个大节的功能。该功能通过使用节名前缀+$+任意字符串 的形式来激活。编译器将合并具有相同节名前缀的节为一个大节。

编译器对于相似节采用字典顺序进行合并(对$后的字符串进行排序)。这意味着在内存中,位于节“.CRT$XLB”中的变量将在位于节“.CRT$XLA”中变量位置的后面,但是在位于节“.CRT$XLZ”中的变量的前面。C运行时库利用编译器的这一特性来创建一个以NULL结尾的TLS回调数组(将节“.CRT$XLZ”中放置一个NULL指针)。因此为了保证声明的函数指针位于TLS回调数组内部,必须将它放在节“.CRT$XLx”中。

但是,创建TLS目录只是编译器和链接器支持__declspec(thread)变量的一部分工作。下一次,我将讨论编译器和链接器通过何种机制来支持对线程局部变量的访问。

原创粉丝点击