线程局部存储,Part 4:访问__declspec(thread)变量

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


昨天,我大致说了下编译器和链接器如何合作来支持TLS,但是并没有讲当访问__declspec(thread)变量时具体底层是个什么样子,或者说是怎么来做到的。

在解释如何访问__declspec(thread)变量的内部工作原理之前,有必要了解下tlssup.c中的几个特殊变量。这些变量被_tls_used引用来创建TLS目录结构。

第一个感兴趣的变量是_tls_index,在线程局部存储解析机制中,几乎每次线程局部变量被访问时改变量都由编译器隐式引用,从而解析出当前线程局部变量的地址(存在不引用该变量的一个例外,后面说)。_tls_index也是tlssup.c中唯一一个采用默认存储类的变量(普通全局变量)。内部,该变量代表本模块(exe或dll等pe文件)的TLS索引。概念上TLS索引和由TlsAlloc分配的索引相似。但是它们并不兼容(即这两个TLS索引不能混用的,因为底层支撑机制不同),模块独有的TLS索引具有更多的支撑代码。稍后将会讲到这些,现在大家先稍等一下。

在tlssup.c文件中,_tls_start和_tls_end变量的定义如下:

/* Special symbols to mark start and end of ThreadLocal Storage area. */

 

#pragma data_seg(".tls")

 

#if defined (_M_IA64) || defined (_M_AMD64)

_CRTALLOC(".tls")

#endif  /*defined (_M_IA64) || defined (_M_AMD64) */

char _tls_start = 0;

 

#pragma data_seg(".tls$ZZZ")

 

#if defined (_M_IA64) || defined (_M_AMD64)

_CRTALLOC(".tls$ZZZ")

#endif  /*defined (_M_IA64) || defined (_M_AMD64) */

char _tls_end = 0;

 

#pragma data_seg()

这段代码创建了两个变量,并将它们放在.tls段的开头和结尾。编译器和链接器将会自动为所有的__declspec(thread)变量放置在默认段.tls段,在最终的PE文件中这些变量将会被放置在_tls_start和_tls_end中间。这两个变量用于告诉链接器TLS存储模板节的边界位置(首地址和结束地址)。映像文件的TLS目录存储了该信息。

现在我们知道了在语言层次上__declspec(thread)是如何来工作的,接下来有必要了解下编译器产生的访问__declspec(thread)变量的支持代码。幸运的是这些支持代码非常直观。考虑如下测试程序:

__declspec(thread) int threadedint = 0;

int __cdecl wmain(int ac,

   wchar_t**av)

{

  threadedint = 42;

   return 0;

}

对于x64系统,编译器将产生如下代码:

mov ecx,DWORD PTR _tls_index

mov rax,QWORD PTR gs:58h

mov edx,OFFSET FLAT:threadedint

mov rax,QWORD PTR [rax+rcx*8]

mov DWORD PTR[rdx+rax], 42

想想前面有介绍gs段寄存器在x64系统中用于引用TEB的首地址。88(0x58)是TEB的ThreadLocalStoragePointer成员的偏移:

+0x058 ThreadLocalStoragePointer : Ptr64 Void

但是,如果我们在运行时查看代码将会是下面这个样子:

mov     ecx,cs:_tls_index

mov     rax,gs:58h

mov     edx,4

mov     rax,[rax+rcx*8]

mov     dwordptr [rdx+rax], 2Ah ; 42

xor     eax,eax

可以发现“threadedint”变量被解析成了一个小值(4)。回忆在单独编译时,mov edx,4指令对应mov edx,OFFSET FLAT:threadedint。

现在,4不是一个平坦地址(我们希望的是一个范围位于可执行文件使用范围的地址)发生了什么事情了?

ok,原来这里链接器玩了一个小把戏。当链接器解析对__declspec(thread)变量的引用时,将偏移假定为相对于.tls节的起始位置。如果检查PE文件中的.tls段,事情将变得更清晰:

0000000001007000 _tls segment para public 'DATA'use64

0000000001007000      assume cs:_tls

0000000001007000    ;org 1007000h

0000000001007000 _tls_start        dd 0

0000000001007004 ; int threadedint

0000000001007004 ?threadedint@@3HA dd 0

0000000001007008 _tls_end          dd 0

“threadedint”相对于.tls节起始位置的偏移确实是4。但是这些仍然没有解释编译器产生的指令如何访问线程局部变量。

这里诀窍就藏在接下来的三条指令当中:

mov     ecx,cs:_tls_index

mov     rax,gs:58h

mov     rax,[rax+rcx*8]

这三条指令获取TEB中ThreadLocalStoragePointer的值并用_tls_index来索引其指向的空间。获得指针代表的地址在使用threadedint进行索引来合成一个完成的访问该线程所有threadedint变量的地址。

(其实可以这样认为:对于每个线程都有新分配了一块和.tls同样大小的内存,用ThreadLocalStoragePointer引用,这样该变量的值和偏移加起来就是变量的地址了)

采用C语言,编译器产生的代码将是下面的样子:

// This represents the ".tls" section

struct _MODULE_TLS_DATA

{

   inttls_start;

   intthreadedint;

   inttls_end;

} MODULE_TLS_DATA, * PMODULE_TLS_DATA;

 

PTEB Teb;

PMODULE_TLS_DATA TlsData;

 

Teb     =NtCurrentTeb();

TlsData = Teb->ThreadLocalStoragePointer[_tls_index ];

 

TlsData->threadedint = 42;

如果之前你使用过显式TLS,这里看起来是非常熟悉的。显式TLS典型范式就是在TLS槽中放置一个结构体的指针,然后在访问线程局部状态,每个线程的结构体实例都通过结构体指针进行访问。这里不同的地方是编译器和链接器合作(加载器)将你从显式进行这些操作中解脱出来;所有你需要做的就是使用__declspec(thread)声明一个变量,然后一切背后的事情就自然发生了。

从代码生成角度来看,隐式TLS变量的工作机制存在一条额外的曲线。你可能注意到示例中为X64版本中访问__declspec(thread)变量的代码;这是因为默认情况下,X86在构建exe文件时包含一个特殊的优化选项(/GA,Optimize for Windows Application,也许是有史以来编译器选项名字中最烂的一个),该优化假定_tls_index为0从而消除了对其的引用过程(这样加快了对线程局部变量的访问)。

该优化仅仅对进程的主模块起作用(一般是exe文件)。该假定成立的原因是加载器按照模块加载顺序为_tls_index指定序列值,而主模块将在第二个被加载,ntdll是第一个加载的模块(显然ntdll中不能使用__declspec(thread)变量,否则该模块将是0索引,即_tls_index值为0)。值得注意的是,在exe具有导出函数且使用了__declspec(thread)变量时,该优化将会导致应用程序随机崩溃。

以备参考,当/GA选项开启时,X86版编译生成如下指令:

mov     eax,large fs:2Ch

mov     ecx,[eax]

mov     dwordptr [ecx+4], 2Ah ; 42

记得在X86系统中,fs的基地址引用TEB的首地址,ThreadLocalStoragePointer所在的偏移为0x2C。

注意这里并没有对_tls_index的引用;编译器假定使用0值。如果是X86平台下构建dll,该优化始终是关闭的,_tls_index将如之前那样来使用。

但是,__declspec(thread)变量背后的事情远不是编译器和链接器能搞定的。某某仍然需要为每个线程分配存储空间,这个某某就是加载器。更多关于加载器在这里所扮演的角色将在下次进行探讨。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值