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

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

 

ELF 对线程局部储存的处理

原作者: UlrichDrepper,RedHatInc.

drepper@redhat.com

Version 0.20

December 21, 2005

基于:

Intel Itanium Processorspecific 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 )。

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

__thread int i;

__thread struct states;

extern __thread char *p;

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

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

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

Ÿ           IA-32

Ÿ           IA-64

Ÿ           SPARC 32 位及 64 位)

Ÿ           SuperHitachi SH

Ÿ           Alpha

Ÿ           X86-64

Ÿ           S390 31 位及 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 a thread-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 所示。

除了程序头项,其他动态链接器需要的信息是,动态段( dynamic section DT_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 the respective 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 thread vector dtvt 的指针。

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

天

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

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

在程序启动时刻,为主线程构建了 TCB 连同动态线程向量。每个模块的 TLS 块的位置,通过使用架构特定的公式,根据各自 TLS 块的尺寸及对齐要求( tlssizex alignx )来计算。在架构特定的段中,该公式将使用一个函数“ 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 需要确定模块 m TLS 所要求的内存,并恰当地初始化它。正如第二节所描述的,有两种数据:已初始化及未初始化。当模块 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 、偏移量,并执行代码放宽( code relaxation )。启动代码除了为初始线程设立 TLS 块外,没有其它任务。线程库也为新创建的线程做同样的事情。这是一个简单的任务,因为只有一个初始化映像。

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

3.4. 架构特定的定义

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

3.4.1. IA-64 细节

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

dtvt 的地址可以通过载入由线程寄存器, tp GR 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_addr IA-64 ABI 中的定义如上所描述:

extern void *__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 的稍有不同。其原型是:

extern void *__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 上,占据 2 GOT 项。

这个函数的定义是区分 2 IA-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 thread pointer. I.e., the content of the word addressed via %gs: 0 is the address of the very same location )这个潜在的好处是巨大的,因为我们可以通过 %gs 寄存器直接访问内存,而不需要首先载入线程指针。下面 x86 的初始及局部执行模式( initial and local exec model )的章节显示了这一好处。

3.4.3. SPARC 细节

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

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

在启动时刻出现的模块的 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 相同的接口。其原型是:

extern void *__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 int 32 SPRAC 上是 4 字节,而在 64 SPARC 上是 8 字节, tls_index 的成员,对于两者 CPU ,都与 GOT 项大小相同,因此同样也可以在 GOT 数据结构上定义这个类型的对象。

3.4.4. SH 细节

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

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

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

3.4.5. Alpha 细节

Alpha ABI IA-64 SPARC ABI 的混合体。其线程局部储存数据结构,遵循上面的版本 I TCB 的大小是 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 定义的那样:

extern void *__tls_get_addr (tls_index *ti);

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

3.4.6. x86-64 细节

x86-64 ABI IA-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 相同的接口。其原型是:

extern void *__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 的线程局部储存数据结构。对于这个 ABI TCB 的大小无关重要。线程指针保存在访问寄存器 %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 。其原型是:

unsigned long 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 接口。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值