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

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

4.2.局部动态TLS模式

局部动态TLS模式是常规动态TLS模式的一个优化。如果编译器认识到,这个线程局部变量的引用所在的对象与其定义所在对象相同,它可以产生遵循这个模式的代码。这包括,比如,具有文件域的线程局部变量,或被定义为保护或隐藏的变量(更多这方面的信息参考:通用ELF ABI规范)。这里我们把这些类型的变量援引为受保护的

提醒一下,一个线程局部变量由模块ID及在该模块的TLS块中的偏移来定义。在确信变量定义在引用它的模块中时,其偏移在链接时刻是已知的。模块ID未知(除非它是主应用,这种情况下可以执行更多的优化)。因此仍然需要调用__tls_get_addr来获得模块ID,并最终分配TLS块。如果__tls_get_addr的参数,通过传入0作为偏移,可以使得函数计算TLS块的起始地址。那么有可能多次重用这个值,通过把受保护的线程局部变量的偏移加上TLS块的起始地址,来访问多个变量。编译器可以很容易且高效地产生这样的代码。

但要记住,通常,如果只有一个受保护的线程局部变量,使用局部动态模式没有真正的好处。[1]这将意味着像常规动态模式的一次__tls_get_addr调用,加上额外的加法来计算地址。但如果多个变量以这个方式处理,天平就开始倾斜了。我们仍然只有一个函数调用,而每个变量增加一个加法运算。因为常规及局部动态模式之间的差别不只是替代部分指令,而是产生相当不同的代码,因此从常规到局部动态模式的优化不能由链接器来执行。必须需要编译器来完成,可能是在程序员的协助下。

在架构特定的描述中例子的实现等同于这些代码片段:

static __thread int x1;

static __thread int x2;

&x1;

&x2;

4.2.1.IA-64局部动态TLS模式

IA-64的指令集使得确定,使用局部动态模式变量的地址的,代码序列比常规动态模式的代码序列要短。另外。变量的偏移不需要由动态链接器计算,并且GOT可以少一个元素。

 

局部动态模式代码序列

初始重定位                    符号

0x00  mov  loc0=gp

0x06  addl  t1=@ltoff (@dtpmod (x)), gp

0x0c  addl  out1=@dtprel (x), r0

      ;;

0x10  ld8   out0=[t1]

0x16  br.callrp=__tls_get_addr

 

R_IA_64_LTOFF_DTPMOD22      x

R_IA_64_DTPREL22              x

 

GOT [n]

未解决的重定位

R_IA_64_DTPMOD64LSB        x

 

与常规动态模式的区别在于,不需要通过把@ltoff (@dtprel(x))的值到gp,然后从这个地址载入,来找出变量的偏移。取而代之,使用地址0x0caddl指令来直接计算偏移(这正是IA-64如何载入一个立即值)。这意味着需要的ld8指令可以少一个,编译器可以把这个槽用于别的东西。

然而这个代码序列有一个限制。在TLS块中的偏移仅有21位。如果线程局部数据的数量超过了221字节(2M),就要使用另外的代码。更大的偏移必须使用长的move指令来载入,这个指令允许载入完整的64位偏移。另外,编译器可以进一步优化addl指令,如果它被知会线程局部变量的需求不会超过213字节(8K)。这些情形下使用的重定位分别将是R_IA_64_DTPREL64IR_IA_64_DTPREL14。不管编译器如何选择,通常不可能由链接器来确定最好或必须的指令,因此选择将由用户使用编译选项来决定。上面例子的代码序列是一个好的折衷,可作为缺省设定。

在一个函数必须访问多个受保护的线程局部变量的情形下,节省会更大。在这种情况下,不调用__tls_get_addr来计算任何变量的地址,而是仅计算TLS块的起始地址。

 

局部动态模式代码序列,II

初始重定位                    符号

0x00  mov  loc0=gp

0x06  addl  t1=@ltoff (@dtpmod (x)), gp

0x0c  addl  out1=@dtprel (x), r0

      ;;

0x10  ld8   out0=[t1]

0x16  br.callrp=__tls_get_addr

      ;;

0x20  mov  gp=loc0

0x26  mov  r2=ret0

      ;;

0x30  addl  loc1=@dtprel (x1), r2

0x36  addl  loc2=@dtprel (x2), r2

 

R_IA_64_LTOFF_DTPMOD22      x1

 

 

 

 

 

 

 

 

R_IA_64_DTPREL22              x1

R_IA_64_DTPREL22              x2

 

GOT [n]

未解决的重定位

R_IA_64_DTPMOD64LSB        x

 

这个代码的第一部分与前面的代码非常相似,前面的代码只使用了一个变量。唯一的区别在于显式指定的0作为第二个参数传递给了__tls_get_addr。这是计算找到x1的模块,即也是这个代码所在模块,的TLS块的开头。

为了完成计算,需要额外的代码,这些代码始于把该函数调用的返回值保存到一个随后可以被使用的地方(寄存器r2)。最后我们看到真正计算变量地址的代码。它非常简单,因为我们仅要把变量的偏移加上TLS块的基址。这个偏移,在链接时刻,是替换了重定位为R_IA_64_DTPREL22的代码的,已知的立即数。正如在上面的代码中,这个重定位是大小和复杂度的一个折衷。这里,编译器同样可以使用短的add指令或长的move指令。

4.2.2.IA-32局部动态TLS模式

局部动态模式的代码序列没有提供比常规动态模式更多的好处,除非使用了多个变量。原因很清楚。调用__tls_get_addr的代码完全没有改变,因为它只是计算GOT项的地址。GOT项必须由两个字组成,尽管字ti_offset在链接时刻已知。在需要多个变量的情况下,使用这个模式会带来好处。下面是Sun版本的代码序列。

 

局部动态模式代码序列

初始重定位                    符号

0x00 leal  x1@tmdnx (%ebx), %edx

0x06 pushl %edx

0x07 call x@TLSPLT

0x0c popl  %edx

   

0x10 movl $x1@dtpoff, %edx

0x15 addl %eax, %edx

0x20 movl $x2@dtpoff, %edx

0x25 addl %eax, %edx

R_386_TLS_LDM_32            x1

R_386_TLS_LDM_PUSH         x1

R_386_TLS_LDM_CALL         x1

R_386_TLS_LDM_POP          x1

 

R_386_TLS_LDO_32            x1

 

 

R_386_TLS_LDO_32            x2

 

GOT [n]

未解决的重定位

R_386_TLS_DTPMOD32        x1

 

在第一条指令中,表达式x1@tmdnx (%ebx)指示汇编器产生一个R_386_TLS_LDM_32。这进而告诉链接器在GOT上构建一个特殊的tls_index对象,其中成员ti_offset0。这就是为什么在上面的代码中,GOT只有一个未解决的重定位。当处理R_396_TLS_DTPMOD32重定位时,成员ti_module将被填入代码所在模块的ID

__tls_get_addr的调用返回时,寄存器%eax包含了,对于当前线程,变量所在模块的TLS块的地址。这时所需要做的是,通过加上变量偏移来完成地址计算。在地址0x100x15的指令,通过把偏移加上寄存器%eax的内容,计算变量x1的地址。为此,使用表达式$x1@dtpoff,它产生一个R_386_TLS_LDO_32重定位。这个重定位引用变量x1,并且其偏移可以由链接器算出,并填入指令。

使用第二个变量仅要求重复加法,它的工作要比函数调用少。并且虽然使用了两个变量,在GOT中仅构建了一个tls_index元素。

对于GNU版本,其代码序列带来的好处更明显。

 

局部动态模式代码序列

初始重定位                    符号

0x00 leal x@tlsldm (, %ebx, 1), %eax

0x06 call ___tls_get_addr@plt

    

0x10 leal x1@dtpoff (%eax), %edx

    

0x20 leal x2@dtpoff (%eax), %edx

R_386_TLS_LDM                x1

R_386_PLT32             ___tls_get_addr

 

R_386_TLS_LDO_32             x1

 

R_386_TLS_LDO_32             x2

 

GOT [n]

未解决的重定位

R_386_TLS_DTPMOD32        x

 

TLS基址的计算遵循Sun的版本,其改进归究于__tls_get_addr的调用规范。GOT包含一个特殊的tls_index项,其中成员ti_offset将是0。唯一的区别在于把表达式x1@tlsldm (%ebx)用于GOT项的地址。这个表达式的处理与x1@tmdnx(%ebx)相似,除了为指令构建的重定位是R_386_TLS_LDM,而不是R_386_TLS_LDM_32

不过调用规范不是仅有的好处。计算最终地址的指令也优化了。利用leal指令的强大功能,在Sun版本值所需的2条指令被合并为1条。该指令的重定位保持不变。不过这还没完。如果不计算变量的地址,这个值就需要是已载入的值,通过使用

movlx1@dtpoff (%eax), %edx

这个指令将得到与原来leal指令相同的重定位。在这样的一个变量中存入值,其行为是相同的。

只要TLS块的基址在寄存器中保护得好好的,保存或计算其地址受保护线程局部变量,只是一条指令的事。

4.2.3.SPARC局部动态TLS模式

对于SPARC,就像IA-32,当仅使用一个变量时,局部动态模式没有好处。对SPARC其坏处甚至更大,这归究于其RISC指令集的本质。如果使用了多个变量,产生的代码看起来如下:

 

局部动态模式代码序列

初始重定位                    符号

0x00 sethi %hi (@tmdnx (x1)), %o0

0x04 add %o0, %lo (@tmndx (x1)), %o0

0x08 add %l7, %o0, %o0

0x0c call __tls_get_addr

     ...

0x10 sethi %hix (@dtpoff (x1)), %l1

0x14 xor  %l1, %lox (@dtpoff (x1)), %l1

0x18 add  %o0, %l1, %l1

     ...

0x20 sethi %hix (@dtpoff (x2)), %l2

0x24 xor  %l2, %lox (@dtpoff (x2)), %l2

0x28 add  %o0, %l2, %l2

R_SPARC_TLS_LDM_HI22          x1

R_SPARC_TLS_LDM_LO10         x1

R_SPARC_TLS_LDM_ADD          x1

R_SPARC_TLS_LDM_CALL         x1

 

R_SPARC_TLS_LDO_HIX22         x1

R_SPARC_TLS_LDO_LOX22        x1

R_SPARC_TLS_LDO_ADD          x1

 

R_SPARC_TLS_LDO_HIX22         x2

R_SPARC_TLS_LDO_LOX22        x2

R_SPRAC_TLS_LDO_ADD          x2

 

GOT [n]

 

GOT [n]

未解决的重定位,32

R_SPARC_TLS_DTPMOD32        x1

未解决的重定位,64

R_SPARC_TLS_DTPMOD64        x1

 

前四条指令基本上与常规动态模式的代码序列等效。不过这个代码使用tmndx(x1),而不是@dtlndx(x),来为符号x产生一个tls_index项,tmndx (x)构建一个特殊的偏移为0引用当前模块(包含x1)的索引。链接器将为这个对象构建一个重定位,依赖于平台,它或者是R_SPARC_TLS_DTPMOD32,或者是R_SPARC_TLS_DTPMOD64。重定位DTPREL是不需要的。

这样做的原因是,偏移是分别载入的。表达式@dtpoff (x1)用于访问符号x1的偏移。使用地址0x100x14的两条指令,完整的偏移被载入,加上在%o0__tls_get_addr的调用结果,产生的结果在%l1中。表达式@dtpoff (x1)分别为%hix ()%lox ()部分,创建重定位R_SPARC_TLS_LDO_HIX22R_SPARC_TLS_LDO_LOX22。指令add被标记上重定位R_SAPCR_TLS_LDO_ADD,因此链接器可以识别它。

使用局部动态模式的好处是,每增加一个变量,只需要添加三条指令,而不需要增加GOT项及运行时重定位。总之,如果运行时处理运行时重定位的开销可以避免,即便只有一个变量,这个模式可能也是可取的。

4.2.4.SH局部动态TLS模式

如同其他架构,在SH中为局部动态模式产生的代码,与常规动态模式的不同之处在于,查找第一个局部符号需要额外的努力。对于第二及以后的查找要轻松得多,尤其对于SH

 

局部动态模式代码序列

初始重定位                    符号

0x00  mov.l lf, r4

0x02  mova 2f, r0

0x04  mov.l 2f, r1

0x06  add  r0, r1

0x08  jsr   @r1

0x0a  add  r12, r4

0x0c  bra  3f

0x0e  nop

.align 2

1:    .long x@tlsgd

2:    .long __tls_get_addr@plt

3:   

     mov.l .Lp, r1

     mov.l r0, r1

    

     mov.l .Lq, r1

     mov.l r0, r1

    

.Lp:  .long x1@dtpoff

.Lq:  .long x2@dtpoff

 

 

 

 

 

 

 

 

 

R_SH_TLS_LD_32                x1

 

 

 

 

 

 

 

 

R_SH_TLS_LDO_32               x1

R_SH_TLS_LDO_32               x2

 

GOT [n]

未解决的重定位

R_SH_TLS_DTPMOD32        x1

 

7条指令与常规动态模式的相同。只是这一次符号查找是特殊的,因为它使用模块的TLS数据段为0的偏移。这与在SPARCIA-32上做的一样。与常规动态模式不同之处在于2GOT项只有一个需要附加重定位。域ti_offset总是0

在这些准备完成后,确定局部变量地址的代码是简单的。它包含在TLS段中载入变量为链接时刻常量的偏移,并加上先前找到的,模块及当前线程的TLS段的起始地址。

与常规动态模式的代码序列比较,两个变量的一次查找节省了三条指令,一个GOT项,及一个函数调用。对于三个TLS变量的查找,节约的将是八条指令,两个GOT项,及两次函数调用。显而易见,一旦使用多个变量,选择局部动态模式有好处。

值得注意的是,在这个代码序列中,对标记着.Lp.Lq的变量偏移的内存分配,可以被推迟并最终与其它数据合并(正如上面的例子代码那样)。在构建了它们之后,就不再需要修改mov.ladd指令。从局部动态模式到局部可执行模式的优化不会触及这些指令。因此它们可以被编译器自由移动,在代码中它们不需要固定的位置。

4.2.5.Alpha局部动态TLS模式

对于Alpha,正如IA-32,当仅使用一个变量时,局部动态模式没有优势。如果使用多个变量,所产生的代码看起来如下:

 

局部动态模式代码序列

初始重定位                    符号

0x00  lad  $16, x($gp)   !tlsldm!1

0x04  ldq  $27, __tls_get_addr ($gp) !literal!1

0x08  jsr   $26, ($27), 0  !lituse_tlsldm!1

0x0c  ldah  $29, 0 ($26)  !gpdisp!2

0x10  lda  $29, 0 ($29)  !gpdisp!2

     

0x20  lda  $1, x1 ($0)   !dtprel

     

0x30  ldah $1, x2 ($0)   !dtprelhi

0x34  lda  $1, x2 ($1)   !dtprello

     

0x40  ldq  $1, x3 ($gp)   !gotdtprel

0x44  addq $0, $1, $1

R_ALPHA_TLSLDM              x

R_ALPHA_LITERAL     __tls_get_addr

R_ALPHA_LITUSE              5

R_ALPHA_GPDISP              4

 

 

R_ALPHA_DTPREL16           x1

 

R_ALPHA_DTPRELHI           x2

R_ALPHA_DTPRELLO          x2

 

R_ALPHA_GOTDTPREL         x3

 

GOT [n]

未解决的重定位

R_ALPHA_DTPMOD64        x

 

0x000x14之间的指令基本上与于常规局部模式相同。不同之处在于使用!tlsldm而不是!tlsgd,它将为当前对象构建一个具有0偏移的tls_index项。

这个偏移随后被加上dtprel重定位中的一个。为此,依赖于期望的TLS数据段的大小,我们有三个代码生成选项可选择。在0x20的序列可用于15位的正位移(32K);在0x30的序列可用于31位的正位移(2G);而在0x40的序列则是用于64位的位移(displacement)。

4.2.6.x86-64局部动态TLS模式

类似于IA-32SPARC,如果仅有一个局部变量以这个方式访问,这个访问模式对常规动态模式没有优势可言。

 

局部动态模式代码序列

初始重定位                    符号

0x00  leaq x1@tlsld (%rip), %rdi

0x07  call __tls_get_add@plt

     

0x10  leaq x1@dtpoff (%rax), %rcx

     

0x20  leaq x2@dtpoff (%rax), %r9

R_X86_64_TLSGD               x1

R_X86_64_PLT32        __tls_get_addr

 

R_X86_64_DTPOFF32            x1

 

R_X86_64_DTPOFF32            x2

 

GOT [n]

未解决的重定位

R_X86_64_DTPMOD64           x1

 

前两条指令与常规动态模式的基本相同,虽然少了填充位。这两条指令必须相连。代码使用x1@tlsld *%rip),而不是x1@tlsgd (%rip),来为符号x1构建tls_index项,它创建了一个特殊的,带有偏移0的引用当前模块(包含x1)的索引。链接器将为这个对象仅创建一个重定位,R_X86_64_DTPMOD64。重定位R_X86_64_DTPOFF64不需要。

这样的原因是偏移是被另外载入的。表达式x1@dtpoff用于访问符号x1的偏移。使用在0x10的指令,完整的偏移被载入,并加上在%rax__tls_get_addr调用的结果,产生的结果则在%rcx中。表达式x1@dtpoff构建了重定位R_X86_64_DTPOFF32。如果不计算变量的地址,这个值就需要是已载入的值,通过使用

movqx1@dtpoff (%rax), %r11

这个指令将得到与原来的leaq指令相同的重定位。在这样的一个变量中存入值,其行为是相同的。

只要TLS块的基址在寄存器中保护得好好的,保存或计算其地址受保护线程局部变量,只是一条指令的事。

使用局部动态模式的好处是,对于每个增加的变量,只需要增加3条新指令,不需要增加GOT项或运行时重定位。总之,如果运行时处理运行时重定位的开销可以避免,即便只有一个变量,这个模式可能也是可取的。

4.2.7.s390局部动态TLS模式

对于s390,如果仅访问一个变量,局部动态模式的代码序列,对于常规动态模式,没有优势。它甚至会稍微更糟糕一些,因为需要载入一个额外的字常数库(literal pool)的项(x@tlsldmx@dtpoff,而不仅是x@ltsgd),并加上__tls_get_offset函数调用的返回值。如果访问多个局部变量,局部动态模式要好于常规动态模式,因为每增加一个变量,仅需要一个简单的字常数库的载入,而不是一整个函数调用。

 

局部动态模式代码序列

初始重定位                    符号

l   %r6, .L1-.L0 (%r13)

ear %r7, %a0

 

 

 

R_390_TLS_LDCALL               x1

 

 

 

 

 

 

 

 

R_390_TLS_LDM32                x1

R_390_TLS_LDO32                x1

R_390_TLS_LDO32                x2

l   %r2, .L2-.L0 (%r13)

bas %r14, 0 (%r6, %r13)

la  %8, 0 (%r2, %r7)

l  %r9, .L3-.L0 (%r13)

la  %r10, 0 (%r10, %r8) # %r10 = &x1

l  %r9, .L4-.L0 (%r13)

la  %r10, 0 (%r10, %r18) # %r10 = &x2

...

.L0 : # literal pool, address in %r13

.L1: .long __tls_get_offset@plt-.L0

.L2: .long x1@ltsldm

.L3: .long x1@dtpoff

.L4: .long x2@dtpoff

 

GOT [n]

未解决的重定位

R_390_TLS_DTPMOD              x1

 

正如IA-32局部动态TLS模式,表达式x1@tlsldm在字常数库中的语义是,指示汇编器发布一个R_390_TLS_LDM32重定位。链接器将为它在GOT上构建一个特殊的tls_index对象,其中成员ti_offset置为0。当处理重定位R_390_TLS_LDM32时,成员ti_module将被填入代码所在模块的ID。字常数库的项x1@dtpoffx2@dtpoff被汇编器翻译为重定位R_390_TSL_LDO32。链接器将为该模块计算TLS块中x1x2的偏移,并写入字常数库。

这个指令序列被分成四部分。第一部分类似于常规动态模式的第一部分。第二部分,使用凭借字常数库项x@tlsldm创建的tls_index对象的偏移,调用__tls_get_offset。在这个调用之前,必须设立GOT寄存器%r12。在第二部分的第三条指令之后,%r8包含了代码所在模块的线程局部内存的地址。代码序列的第三部分显示了如何计算线程局部变量x1x2的地址。第四部分显示了代码序列所需要的字常数库的项。

局部动态访问模式的所有指令都可以被编译器自由地安排,只要满足明显的数据依赖,并且考虑到了bas指令的函数调用语义。

4.2.8.s390x局部动态TLS模式

s390x的局部动态访问模式类似于s390的版本。在s390s390s的常规动态模式之间的差别依然存在。线程指针的提取要求3条指令而不是1条,使用bras1指令来完成到__tls_get_offset的跳转,并且偏移是64位的而不是32位。

 

局部动态模式代码序列

初始重定位                    符号

ear  %r7, %a0

sllg  %r7, %r7, 32

ear  %r7, %a1

 

 

 

 

R_390_TLS_LDCALL               x1

 

 

 

 

 

 

 

R_390_TLS_LDM64                 x1

R_390_TLS_LDO64                 x1

R_390_TLS_LDO64                 x2

lg   %r2, .L2-.L0 (%r13)

bras1 %r14, __tls_get_offset@plt

la   %r8, 0 (%r2, %r7)

lg  %9, .L2-.L0 (%r13)

la  %r10, 0 (%r9, %r8) # %r10 = &x1

lg  %r9, .L3-.L0 (%r13)

la  %r10, 0 (%r9, %r8) # %r10 = &x2

...

.L0 : # literal pool, address in %r13

.L1: .quad x1@tlsldm

.L2: .quad x1@dtpoff

.L3: .quad x2@dtpoff

 

GOT [n]

未解决的重定位

R_390_TLS_DTPMOD               x1

 



[1] IA-64具有优势

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 想去美国开饭店要怎么办签证 在沙漠中旅游如果车子坏了该怎么办 小磨床平面磨出来很粗怎么办 玩cs鼠标单点总是连点怎么办 论文出现计算上的错误该怎么办 不戴头盔违法扣分怎么办不了缴费 去法国会说英语不会说法语怎么办 企业有很多费用没有正式发票怎么办 我二张一万元的作废发票掉了怎么办 小规模上月开错税率的票怎么办 一般纳税人开票税率开错了怎么办 电信电子发票代表号码错了怎么办 发票备注栏项目名称写了两遍怎么办 苏州出入境手撕发票弄丢了怎么办 电脑下载过播放器还是不播放怎么办 局域网内的电脑无法互相访问怎么办 浏览器拖动滑动线页面会跳动怎么办 在暴风影音上视频播放不出来怎么办 格式工厂转换格式占内存太大怎么办 手机登陆不上路由器管理界面怎么办 新路由器设置密码后不能上网怎么办 更换网络后无线路由器不能用怎么办 手机登录不了路由器登录业面怎么办 海康威视通道用户被锁定怎么办 无线适配器或访问点有问题怎么办 客户买鞋子说价格贵该怎么办 小米5s刷成真砖后怎么办 手机电源键坏了开不了机怎么办 手机开关键坏了开不了机怎么办 vivo手机解屏密码忘了怎么办 小米max关机后开关键坏了怎么办 联想手机刷机失败无限重启怎么办 红米2a显示白屏怎么办 小米6手机一直处于开机状态怎么办 怎么在手机上看wifi密码怎么办 怎么查自己的宽带密码忘记了怎么办 怎么查自己宽带账号密码忘了怎么办 电脑重置路由器密码连不上网怎么办 e盘和f盘没有了怎么办 复制文件过程中自己卡住了怎么办 电脑卡住了怎么办 鼠标也点不动