ELF对线程局部储存的处理(1)

来源:互联网 发布:mac popo登录408 编辑:程序博客网 时间:2024/06/06 12:37

一周之前,正在为GCC中对线程局部变量的处理而头疼不已,偶尔在文档《Using Gcc》里找到了这篇“ELFHandling For Thread-Local Storage”,它对线程局部变量的描述澄清了我的不少疑问,考虑到尚未看到中文的版本,特把它翻译了出来。当然这里的描述距离真正的代码实现还很远,不过从中已可窥探出,现代编译器、链接器确实是充满挑战、令人兴奋的软件。

 

ELF对线程局部储存的处理

原作者:UlrichDrepper,RedHatInc.

drepper@redhat.com

Version0.20

December21, 2005

基于:

Intel ItaniumProcessorspecific Application Binary Interface, May2001, Document Number: 245370-003

Thread-LocalStorage, Version 0.68, Sun Microsystems,September 4, 2001

1.      动机

线程使用的增加导致开发者期望有更好的方式来处理线程局部数据。POSIX线程接口定义了,允许独立于每个线程的void *对象的,存储。不过这个接口的使用很累赘。在运行时需要为对象分配一个键(key),如果这个键不再使用就要释放它。这已经是一大堆工作,而且容易出错。在结合了动态加载代码(dynamically loaded code)后,这就成了麻烦的问题(a real problem)。

为了解决这个问题,最后办法是扩展编程语言,让编译器来接手这个工作。对于CC++,可以在变量的定义及声明中使用新关键字__thread。这不是语言的一个正式(official)扩展,但编译器的作者被鼓励实现它们来支持新的ABI。以这个方式定义及声明的变量将,在每个线程中,被自动局部地分配:

__thread int i;

__thread struct states;

extern__thread char *p;

其有用性不局限域用户程序(user-program)。运行时环境亦可以获得好处(比方说,全局变量errno必须是线程局部的),而且编译器可以执行那些构建非自动变量(non-autimaticvariable)的优化。注意在一个自动变量的定义中,加入__thread是不合理的,且不被允许,因为自动变量总是线程局部的。另一方面,函数作用域中的静态变量则是候选者。

为了实现这个新的特性,运行时环境必须被改变。必须扩展二进制格式,把线程局部变量的定义与普通变量的分开。动态加载器必须能够初始化这些特殊的数据段(data section)。线程库必须被修改,以对新线程分配新的线程局部数据段(thread-local data section)。本文将描述对ELF格式的改变,及运行时环境需要做什么。

当前不是所有具有ELF格式的架构都被支持。被支持及在本文中描述的架构有:

Ÿ          IA-32

Ÿ          IA-64

Ÿ          SPARC32位及64位)

Ÿ          SuperHitachiSH

Ÿ          Alpha

Ÿ          X86-64

Ÿ          S39031位及64位)

HP/PA 64位的描述等待加入这个文档,其它架构在写这个文档的时刻还没有(完成)支持。

1:对于.tbss.tdata的段表项(section table entry

.tbss

.tdata

sh_name

.tbss

.tdata

sh_type

SHT_NOBITS

SHT_PROGBITS

sh_flags

SHF_ALLOC + SHF_WRITE + SHF_TLS

SHF_ALLOC + SHF_WRITE + SHF_TLS

sh_add

段的虚址

段的虚址

sh_offset

0

初始化映像的文件偏移(file offset of initialization image

sh_size

段的大小

段的大小

sh_link

SHN_UNDEF

SHN_UNDEF

sh_info

0

0

sh_addralign

段的对齐量

段的对齐量

sh_entsize

0

0

2.      数据定义

要求发布(emit)线程局部数据对象的改变是最小的。线程局部的数据将出现在.tdata.tbss中,而不是分别用于初始化及非初始化数据的.data.bss段。这些段就像它们的非线程对手那样定义,而在段的标识(flags)中多设定了一个的标记。用于这些段的段表项如表1所示。可以看到,与普通数据段唯一的不同在,于设置了SHF_TLS标记。

这些段的名字,理论上对于ELF文件中所有段,是不重要的。取而代之,链接器将把所有设置了SHF_TLS标记的SHT_PROGBITS类型的段视作.tdata段,而把所有设置了SHF_TLS标记的SHT_NOBITS类型的段视为.tbss段。确保其他域符合表1的描述,是输入文件作者的责任。

不同于普通的.data段,运行程序是不会直接使用.tdata段的。这个段可能在启动期间,由动态链接器通过执行重定位来修改。不过在这之后,这个段的数据被保留为初始化映像(initialization image),并且不再改变。对于每个线程,包括初始化线程,被分配新的内存,然后拷贝这个初始化映像。这保证了所有的线程都有相同的初始条件。

因为任何一个线程局部变量符号(any symbol for athread-local variable)都没有关联的地址,不能使用通常使用的符号表项(symbol table entry)。在执行映像中,域st_value将包含变量在运行时的绝对地址;在DSO中,这个值将是相对于加载地址的。两者对于TLS变量都不可用的。出于这个原因,引入了一个新的符号类型——STT_TLS。这个类型的项为所有引用线程局部储存的符号所创建。在目标文件中,st_value域将包含惯常的,到由st_shndx域所引用段开头的偏移。在执行映像及DSO中,st_value域包含了该变量在TLS初始化映像中的偏移。

2:用于初始化映像的程序头表项

p_type

PT_TLS

p_offset

TLS初始化映像的文件偏移(file offset of the TLS initialization image

p_vaddr

TLS初始化映像的虚地址

p_paddr

保留未用

p_filesz

TLS初始化映像的大小

p_memsz

TLS初始化映像的总体大小

p_flags

PF_R

p_align

TLS初始化映像的对齐量

允许使用STT_TLS符号的,仅是那些被引入来处理TLS的重定位。这些重定位不使用其他类型的符号。

为了允许动态链接器执行这个初始化,初始化映像的位置必须在运行时已知。段头(section header)是不可用的;取而代之,是构建一个新的程序头项(program header entry)。其内容如表2所示。

除了程序头项,其他动态链接器需要的信息是,动态段(dynamicsectionDT_FLAGS项中的,DF_STATIC_TLS标识。这个标识允许拒绝动态加载,以静态模式(static model)创建的模块。下一节将介绍这两个模式。

每个线程局部变量通过到线程局部储存段(在内存中,.tbss段遵循对齐要求被直接分配在.tdata段后)头的偏移所识别。在链接时刻,没有虚地址可以被计算。这即便对于执行映像亦如是,否则它已经完成了重定位。

3.      TLS的运行时处理

如上面提到的,线程局部储存的处理不像普通数据那么简单。数据段不可以简单地向进程开放。相反要构建多个拷贝,它们都从同一个初始化映像初始化得到。

另外,运行时支持应该避免构建不必要的线程局部储存。例如,一个加载模块仅被构成进程的多个线程中的一个使用。为所有的线程分配储存,将浪费内存及时间。一个懒惰的方法(lazy method)是被希冀的。这不是个太大的额外负担,因为动态加载对象的处理已经要求识别还未分配的存储。这是唯一可以替代暂停所有线程在它们重新运行前分配储存的做法。

我们将看到出于性能的原因,不是总能够使用线程局部储存的懒惰分配。至少应用本身及由DSO初始加载的线程局部储存总是马上就分配。

即便分配了内存,使用线程局部储存带来的问题还没完。ELF二进制格式定义的符号查找规则不允许,在链接时刻鉴定包含已使用定义的对象。并且如果该对象不是已知的,在线程局部储存段中的,这个对象的偏移亦不可确定。因而通常的链接过程不会发生。

一个线程局部的变量可以,通过对该对象的一个引用(因而该对象的线程局部储存段),及该变量在这个线程局部储存段的偏移,来识别。为了把这些值映射到实际的虚地址,运行时需要一些现时并不存在的数据结构。它们必须允许把对象的引用映射到,当前线程模块的线程局部储存段的,一个地址(They must allow to map the object reference to an address for therespective thread-local storage section of the module for the current thread)。为此,当前定义了两个版本。不同架构的ABI的细节要求两个版本(variant)。[1]



[1] 使用版本II的一个原因是,出于历史原因,由线程寄存器所指向的内存的布局与版本I不兼容。

它

用于线程局部储存数据结构的版本I(见图1),作为IA-64 ABI的一部分发展而来。作为崭新的定义,兼容不是问题。用于线程t的线程寄存器,由tpt表示。它指向一个线程控制块(TCB),在它偏移为0的位置,包含了一个指向该线程的动态线程向量(dynamic threadvectordtvt的指针。

动态线程向量在其第一个域包含了一个世代号(generationnumbergent,它用于dtvt的延迟调整(deferred resizing)及下面描述的TLS块的分配。而其它域包含了,指向不同的载入模块的,TLS块的指针。在启动时刻载入模块的TLS块直接跟在TCB后,因而具有一个,因架构而异,从线程指针地址开始的,固定偏移。对于所有一开始就存在的模块,在程序启动后,任意TLS块到TCB的偏移(因而线程局部变量)必须是固定的。

天

版本II具有相似的结构。唯一的区别在于,线程指针指向一个未指定大小及内容的线程控制块。TCB的某处包含了一个指向动态线程向量的指针,但未指出某处是何处。这由运行时环境操控,并且该指针不能被假定为可直接访问;编译器不允许产生直接访问dtvt的代码。

用于执行映像本身,及在启动时加载的所有模块的TLS块,都在线程指针所指向地址之下。这允许编译器产生直接访问这块内存的代码。通过动态线程向量访问TLS块也是可能的,它具有与版本I相同的结构,但它亦相对于线程指针,有在程序启动后即固定的偏移。在链接时刻,执行映像本身的TLS数据的偏移是已知的。

在程序启动时刻,为主线程构建了TCB连同动态线程向量。每个模块的TLS块的位置,通过使用架构特定的公式,根据各自TLS块的尺寸及对齐要求(tlssizexalignx)来计算。在架构特定的段中,该公式将使用一个函数“round”,它返回第一个参数取整到其第二个参数整数倍的值:

round (x, y) = y * x/y

TLS块的内存不需要马上就分配。这依赖于模块编译使用的是静态或动态模式,而不管这是否有必要。如果使用的是静态模式,在程序启动时刻,由动态链接器根据重定位来计算地址(更准确些,到线程指针tpt的偏移),并且编译器产生,直接使用这些偏移来查找变量地址的,代码。在这个情形下,内存必须被马上分配。在动态模式中,查找变量地址被推迟到一个由运行时环境提供的名为__tls_get_addr的函数中。这个函数也可以分配及初始化必要的内存。

3.1. 启动及之后

对于使用线程局部储存的程序,启动代码必须在转交控制权之前,为初始线程设置内存。在静态链接的应用中,对线程局部储存的支持是有限的。某些平台(像IA-64)没有在ABI中定义静态链接(如果支持也不是标准的),其他平台像Sun,不鼓励使用静态链接,因为只有有限的功能可用。在任何情况下,在静态链接的代码中,动态加载模块受到很大的限制,甚至是不可能的。因此,处理线程局部储存要简单得多,因为只存在一个模块——执行映像本身。

在动态链接的代码中,处理线程局部储存则要有趣得多。在这个情形下,动态链接器必须包括对这种数据段处理的支持。动态加载使用线程局部储存的代码所提出的要求,在下一节中描述。

为了给线程局部储存设立内存,动态链接器从PT_TLS程序头项(参见表2)获取关于每个模块的线程局部储存的信息。收集所有模块的信息,可以通过一个包含如下内容的记录的链表来处理:

Ÿ          一个指向TLS初始化映像的指针,

Ÿ          TLS初始化映像的大小,

Ÿ          模块的tlsoffsetm

Ÿ          显示模块是否使用静态TLS模式的标识(仅当架构支持静态TLS模式)。

当动态加载另外的模块时,这个链表可以被延长(参见下一节),并且它将被线程库用来为新创建的线程设置TLS块。还有可能合并初始模块集中的两个或更多的初始化记录,以缩短这个链表。

如果所有的TLS内存要在启动时刻分配,其总尺寸将是tlssizes = tlsoffsetM +tlssizeM,其中M是启动时刻的模块数目。不需要马上分配所有的内存,除非有一个模块是以静态模式编译的。如果所有的模块都使用动态模式,就可能推迟分配。一个优化的实现将不会盲目地追随,显示静态模式使用情况的标志。如果所要求的内存不大,就不值得推迟分配,这样甚至可能节省时间及资源。

正如在本节开头解释的那样,一个在线程局部储存中的变量,由一个模块的引用及TLS块中的偏移所指定。给定动态线程向量数据结构,我们可以把模块引用定义作一个以1开始的整数,它可以被用作dtvt数组的索引。每个模块接收到的数字由运行时环境决定。只是执行映像本身必须收到一个固定的数,1,并且其他加载的模块接收到的数不相同。

因此计算一个TLS变量的线程特定地址,是一个简单的操作,它可以由编译器使用版本I产生的代码来执行。但是遵循版本II架构的编译器不能这样做,不这样做也有一个很好的理由:延迟分配(参见下面)。

作为替代,定义了名为__tls_get_addr的函数,理论上它被像这样实现(这是这个函数在IA-64上的形式;其它架构可能使用不同的接口):

void *

__tls_get_addr (size_t m, size_t offset)

{

   char *tls_block = dtv[thread_id][m];

   return tls_block + offset;

}

如何放置向量dtv[thread_id]是特定于架构的。描述ABI架构相关部分的章节将给出一些例子。应该把表达式dtv[thread_id]视为该进程的一个符号化的表示。m是模块ID,在该模块(应用本身或一个DSO)加载的时候,由动态链接器分配。

使用__tls_get_addr函数,还对实现动态模式带来额外的好处,这个模式把TLS块的分配推迟到第一次使用。对此,我们只要使用一个特殊的值填写dtv[thread_id]向量,这个值能与其它普通的值区分,并且它很可能表示一个空的项。改变__tls_get_addr的实现来完成这个额外的工作很简单:

void *

__tls_get_addr (size_t m, size_t offset)

{

   char *tls_block = dtv[thread_id][m];

   if (tls_block ==UNALLOCATED_TLS_BLOCK)

      tls_block =dtv[thread_id][m] = allocate_tls (m);

   return tls_block + offset;

}

函数allocate_tls需要确定模块mTLS所要求的内存,并恰当地初始化它。正如第二节所描述的,有两种数据:已初始化及未初始化。当模块m被加载时,已初始化的数据必须从重定位后的初始化映像中拷贝。未初始化的数据必须被置为0。一个实现可能看起来像这样:

void *

allocate_tls (size_t m)

{

  void *mem = malloc(tlssize[m]);

  memset (mempcpy (mem,tlsinit_image[m], tlsinit_size[m]),

          ‘/0’, tlssize[m] –tlsinit_size[m]);

  return mem;

}

tlssize[m]tlsinit_size[m]tlsinit_image[m]必须以一个依赖于实现的方式来确定。在模块m被加载后,它们都是已知的。注意到同样的映像tlsinit_image[m]被用于所有的线程,在它们创建的时候。一个线程不从其父亲处继承这个数据。

存储数据结构的这两个版本都允许使用静态模式。以这个方式编译的模块可以由动态段(dynamic section)的DT_FLAGS项的DF_STATIC_TLS标志来识别。如果这样的一个模块是初始模块集的一部分(记住,这样的模块不能被动态加载),用于TLS块的内存必须马上为启动时刻的初始线程,及为以后每个新创建的线程分配。否则,分配可被推迟,并且把dtvt的元素设置为一个由实现定义的值(上面的例子中是UNALLOCATED_TLS_BLOCK)。

3.2. 动态加载

模块的动态加载增加了更多的复杂性。首先,不应该限制,在某一时刻能被加载的,使用线程局部储存的模块数目,这意味着在需要时,dtvt数组可以被延长。其次,要绝对地避免内存泄露。当优化实现的速度时,必须要牢牢记住这一点。当释放一个被卸载模块的TLS块的内存时,浮现了速度问题。动态线程向量中的槽,迟早会被重用的。不这样做意味着,当加载新的模块时,总是延长这个向量。

因为释放及重新分配内存代价高昂,尤其是必须为每个线程都这样做,通过循环使用内存希望避免这个代价。但是如果同一个模块多次加载、卸载,必须不会导致内存泄露。

现在实现的限制已经明确了,必须描述需要执行的工作。动态加载包含线程局部储存的模块要求,为应用使用的,使用了这个内存的,当前及将来运行线程,进行准备。注意到加载本身不使用线程局部储存的模块,不管程序余下的部分是否使用线程局部储存,不要求特别的关注。新TLS块的信息必须被加入初始化记录链表中,并且增加已加载模块的计数M。除了今后被创建的线程,已经在运行的线程也要做准备。

加载一个新模块可以导致,为给定线程分配的动态线程向量可能太小,这样的结果。这就是每个dtvt中的世代计数gent所要检测的。如果访问这个向量,首先做的第一件事是确定世代数目是最新的,如果不是,分配一个更大的向量。尽管理论上,这可以由创建新线程的线程(或新线程本身)来完成,但这将导致同步问题,并且如果线程不使用任何线程局部储存,会带来不必要的工作。因为动态加载的模块不能使用静态模式,不需要马上就在dtvt中分配新元素。总是可以把分配推迟到第一次的使用,在那里会调用__tls_get_addr

3.3. 静态链接的应用

在静态链接的应用中处理TLS,要远比在动态链接的代码中简单。最甚,如果确定静态链接的应用不能动态加载模块。即便在某些环境下允许动态加载的系统中,动态加载可能被局限在加载非常基本的模块,而不允许加载使用或定义了线程局部储存的模块。

因此静态链接的代码总是只有一个TLS块。而且因为仅有一个模块在使用,变量的偏移不是问题。因为所有的线程局部变量都要包含在这个唯一的TLS块里,偏移在链接时刻就是已知的。

链接器总是可以填入模块ID、偏移量,并执行代码放宽(coderelaxation)。启动代码除了为初始线程设立TLS块外,没有其它任务。线程库也为新创建的线程做同样的事情。这是一个简单的任务,因为只有一个初始化映像。

从这一节的讨论中,我们已经看到访问TLS块非常简单,因为tlsoffset1的值在链接时刻就知道了,把线程指针,tlsoffset1的值,及变量偏移相加,就得到变量的地址。对于某些架构,链接器可以通过改写编译器产生的代码,自动地帮助代码改进。在讨论线程局部储存访问模式时,我们将看到代码会得到何等简化。而当讨论链接器放宽(linker relaxation)时,我们将看到链接器如何执行所有需要的优化。

3.4. 架构特定的定义

不是所有的架构都使用同一个版本的线程局部储存数据结构,并且某些要求也不同。线程指针的处理是如此的“低级”(low-level),它本质上是特定于架构的。本节描述这些细节来填补到目前为止讨论的空缺,并且为描述启动代码的工作做准备。

3.4.1. IA-64细节

IA-64 ABI指定使用上面版本I的线程局部储存数据结构。TCB的大小是16字节,其中前8个字节包含了指向动态线程向量的指针。余下8个字节保留为实现使用。

dtvt的地址可以通过载入由线程寄存器,tpGR 13)所指向的字tpt来确定。dtvt每个元素的大小是8字节,可以容纳一个指针。

在启动时刻出现的所有模块(即,那些不能被卸载的模块)的TLS块跟在TCB后创建。其tlsoffsetx的值计算如下:

tlsoffset1 = round (16, align1)

tlsoffsetm+1 = round (tlsoffsetm + tlssizem, alignm+1)

对于所有m,有1<= m <= M,其中M是模块的总数。

函数__tls_get_addrIA-64 ABI中的定义如上所描述:

externvoid *__tls_get_addr (size_t m, size_t offset);

它把模块ID及偏移用作参数,这要求重定位改变调用代码以提供需要的信息。

3.4.2. IA-32细节

IA-32 ABI指定使用版本II的线程局部储存数据结构。注意:IA-32 ABI有两个版本(version)。在这两个模式间数据结构的布局没有不同。对于这两个ABI来说,TCB的大小无关紧要。编译器产生的代码不能直接访问动态线程向量。dtvt的每个元素是4字节大小,用作一个指针足够了,用于世代计数也足够。

因为IA-32架构的寄存器不多,线程寄存器通过段寄(segment)存器%gs间接编码得到。对线程寄存器的唯一要求是:实际的线程指针tpt可以通过%gs寄存器从绝对0地址载入。下面的代码将把线程指针载入%eax寄存器:

movl%gs: 0, %eax

为了访问使用静态模式模块的TLS块,必须知道偏移tlsoffsetm。必须从线程寄存器值中减去这些值。这不同于IA-64,在那里偏移是被加上。这些偏移的计算如下:

tlsoffset1 = round (tlssize1, align1)

tlsoffsetm+1 = round (tlsoffsetm + tlssizem+1, alignm+1)

对于所有m,有1<= m <= M,其中M是模块的总数。这些公式与IA-64的稍有不同,因为这些值是要被减去的。

函数__tls_get_addr同样与IA-64的稍有不同。其原型是:

externvoid *__tls_get_addr (tls_index *ti);

其中类型tls_index被定义如下:

typedef struct

{

  unsigned long int ti_module;

  unsigned long int ti_offset;

} tls_index;

成员名出于解释(presentation)的目的给出。它们在运行时环境外不可用。传递给函数的信息,与这个函数的IA-64版本相同,但只需要产生传递一个参数的代码,并且这些值不需要由调用代码从GOT载入。相反,这都集中在__tls_get_addr中。注意到这个结构体的成员的大小与GOT单个项的大小相同。因此这样的一个结构体可以定义在GOT上,占据2GOT项。

这个函数的定义是区分2IA-32 ABI的特征之一。由Sun Microsystems定义的ABI对这个函数使用传统的IA-32调用规范,通过栈传递参数。GNU版本的ABI则定义通过%eax寄存器传递参数。为了避免与Sun接口的冲突,这个函数有一个另外的名字(注意前导的3个下划线):

extern void *___tls_get_addr (tls_index *ti)

   __attribute__((__regparm__ (1)));

这个声明使用了GNU C编译器的记法。函数本身的差别不是很大,但是链接器操作的复杂性及产生代码的大小则有大的差异,GNU版本要好些。

对于在GNU系统上的实现,我们可以增加一个要求。%gs: 0所代表的地址,实际上就是线程指针。即,%gs: 0所指向字的内容就是这个字的地址。(The address %gs: 0 represents is actually the same as the threadpointer. I.e., the content of the word addressed via %gs: 0 is the address ofthe very same location)这个潜在的好处是巨大的,因为我们可以通过%gs寄存器直接访问内存,而不需要首先载入线程指针。下面x86的初始及局部执行模式(initialand local exec model)的章节显示了这一好处。

3.4.3. SPARC细节

SPARC ABIIA-32 ABI几乎完全相同。两者都是由Sun设计的。32位及64SPARC实现的差别在于包含指针的变量的大小不同。

正如IA-32TCB的结构体没有指定。%g7寄存器被用作包含tpt的线程寄存器。在线程寄存器的协助下访问动态线程向量的行为由实现定义。dtvt每个元素的大小,对于32SPARC4字节,对于64SPARC8字节。

在启动时刻出现的模块的TLS块,根据版本II的数据结构布局来分配,并且32位,64位代码都使用相同的公式计算偏移。

tlsoffset1 = round (tlssize1, align1)

tlsoffsetm+1 = round (tlsoffsetm + tlssizem+1, alignm+1)

对于所有m,有1<= m <= M,其中M是模块的总数。

函数__tls_get_addr具有与IA-32相同的接口。其原型是:

externvoid *__tls_get_addr (tls_index *ti);

其中类型tls_index被定义如下:

typedef struct

{

  unsigned long int ti_module;

  unsigned long int ti_offset;

} tls_index;

这里成员名同样仅出于解释(presentation)的目的给出。它们在运行时环境外不可用。

因为类型unsigned long int32SPRAC上是4字节,而在64SPARC上是8字节,tls_index的成员,对于两者CPU,都与GOT项大小相同,因此同样也可以在GOT数据结构上定义这个类型的对象。

3.4.4. SH细节

SH ABIKaz Kojima按照版本I来设计。当前还没有对64SH架构的支持。函数__tls_get_addr具有与SPARC相同的接口:

externvoid *__tls_get_addr (tls_index *ti);

其中类型tls_index被定义如下:

typedef struct

{

  unsigned long int ti_module;

  unsigned long int ti_offset;

} tls_index;

这里成员名如常仅出于解释(presentation)的目的给出。它们在运行时环境外不可用。

当前所支持的SH ABI的细节,因为处理器架构的原因,不同于SPARCIA-32IA-64的代码。在SH-5之前的处理器仅提供非常受限的取址模式,它仅允许最多12位的偏移。因为编译器不能对函数的大小及布局做任何假定(因而符号的相对位置),对象及函数的地址通常不能在运行时计算(译:似乎应该是编译时刻)。相反,地址被保存在变量中,在加载时刻,由运行时链接器计算这些值。这只需要为数据对象定义重定位类型。因为仅需要少数新的重定位类型,这极大地简化了TLS的处理。

访问TLS的代码序列是固定的。指令调度不被允许。在今天的SH实现中,这已不再需要,因为它们不再突出(feature)复杂的乱序执行(out-of-order execution)。

3.4.5. Alpha细节

Alpha ABIIA-64SPARC ABI的混合体。其线程局部储存数据结构,遵循上面的版本ITCB的大小是16字节,其中前8个字节包含了指向动态线程向量的指针。余下8个字节保留为实现使用。

在启动时刻出现的所有模块(即,那些不能卸载的)的TLS跟在TCB后连续构建。其tlsoffsetx的值计算如下:

tlsoffset1 = round (16, align1)

tlsoffsetm+1 = round (tlsoffsetm + tlssizem, alignm+1)

对于所有m,有1<= m <= M,其中M是模块的总数。

函数__tls_get_addr如为SPARC定义的那样:

externvoid *__tls_get_addr (tls_index *ti);

线程指针被保存指针线程的进程控制块中。这个值通过PALcode的入口点PAL_rduniq来访问。

3.4.6. x86-64细节

x86-64 ABIIA-32 ABI几乎完全相同。差别主要在于,包含指针变量的不同的大小,并且只有一个更接近IA-32 GNU版本的版本。

它使用%fs段寄存器,而不是%gs段寄存器。在线程寄存器的协助下,访问动态线程向量的行为,由实现定义。dtvt每个元素的大小是8字节。

在启动时刻出现的所有模块的TLS块,根据版本II的数据结构布局来分配,并使用相同的公式计算偏移。

tlsoffset1 = round (tlssize1, align1)

tlsoffsetm+1 = round (tlsoffsetm + tlssizem+1, alignm+1)

对于所有m,有1<= m <= M,其中M是模块的总数。

函数__tls_get_addr具有与IA-32相同的接口。其原型是:

externvoid *__tls_get_addr (tls_index *ti);

其中类型tls_index被定义如下:

typedef struct

{

  unsigned long int ti_module;

  unsigned long int ti_offset;

} tls_index;

这里成员名同样仅出于解释(presentation)的目的给出。它们在运行时环境外不可用。

3.4.7. s390细节

s390 ABI使用版本II的线程局部储存数据结构。对于这个ABITCB的大小无关重要。线程指针保存在访问寄存器%a0里,在能作为地址使用前,需要被提取入一个通用寄存器。从%a0获取线程指针到,比如,%r1的一个方法是使用ear指令:

ear%r1, %a0

在启动时刻出现的所有模块的TLS块根据版本II的数据结构的布局来分配,并且使用相同的公式计算偏移。tlsoffseti的值必须从线程寄存器的值中减去。

tlsoffset1 = round (tlssize1, align1)

tlsoffsetm+1 = round (tlsoffsetm + tlssizem+1, alignm+1)

对于所有m,有1<= m <= M,其中M是模块的总数。

S390 ABI定义使用函数__tls_get_offset,而不是其它ABI所使用的函数__tls_get_addr。其原型是:

unsignedlong int __tls_get_offset (unsigned long int offset);

这个函数具有隐藏的第二个参数。调用者需要设立GOT寄存器%r12,来包含调用者模块的全局偏移表(global offset table)的地址。参数offset,当加上GOT寄存器的值时,得到,位于调用者的全局偏移表中的,tls_index结构体的地址。类型tls_index被定义如下:

typedef struct

{

  unsigned long int ti_module;

  unsigned long int ti_offset;

} tls_index;

__tls_get_offset的返回值是线程指针的一个偏移。为了得到所要求的变量的地址,线程指针需要加上返回值。__tls_get_offset的使用看起来似乎比标准的__tls_get_addr更复杂,但是对于s390,使用__tls_get_offset产生更好的代码序列。

3.4.8. s390x细节

s390x ABI非常接近于s390 ABI。线程局部储存数据结构遵循版本II。对于这个ABI来说,TCB的大小无关紧要。线程指针被保存在访问寄存器对(access register pair%a0%a1中,其中线程指针的高32位在%a0里,低32位在%a1中。把线程指针获取入,比如寄存器,%r1的一个方法是使用下面的指令序列:

ear %1, %a0

sllg %r1, %r1, 32

ear %r1, %a1

在启动时刻出现的所有模块的TLS块,使用与s390相同的公式计算tlsoffsetm,并且s390x ABI使用与s390相同的__tls_get_offset接口。