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

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 ,然后从这个地址载入,来找出变量的偏移。取而代之,使用地址 0x0c addl 指令来直接计算偏移(这正是 IA-64 如何载入一个立即值)。这意味着需要的 ld8 指令可以少一个,编译器可以把这个槽用于别的东西。

然而这个代码序列有一个限制。在 TLS 块中的偏移仅有 21 位。如果线程局部数据的数量超过了 221 字节( 2M ),就要使用另外的代码。更大的偏移必须使用长的 move 指令来载入,这个指令允许载入完整的 64 位偏移。另外,编译器可以进一步优化 addl 指令,如果它被知会线程局部变量的需求不会超过 213 字节( 8K )。这些情形下使用的重定位分别将是 R_IA_64_DTPREL64I R_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_offset 0 。这就是为什么在上面的代码中, GOT 只有一个未解决的重定位。当处理 R_396_TLS_DTPMOD32 重定位时,成员 ti_module 将被填入代码所在模块的 ID

__tls_get_addr 的调用返回时,寄存器 %eax 包含了,对于当前线程,变量所在模块的 TLS 块的地址。这时所需要做的是,通过加上变量偏移来完成地址计算。在地址 0x10 0x15 的指令,通过把偏移加上寄存器 %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 条。该指令的重定位保持不变。不过这还没完。如果不计算变量的地址,这个值就需要是已载入的值,通过使用

movl x1@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 的偏移。使用地址 0x10 0x14 的两条指令,完整的偏移被载入,加上在 %o0 __tls_get_addr 的调用结果,产生的结果在 %l1 中。表达式 @dtpoff (x1) 分别为 %hix () %lox () 部分,创建重定位 R_SPARC_TLS_LDO_HIX22 R_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 的偏移。这与在 SPARC IA-32 上做的一样。与常规动态模式不同之处在于 2 GOT 项只有一个需要附加重定位。域 ti_offset 总是 0

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

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

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

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

 

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

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

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

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

 

局部动态模式代码序列

初始重定位                     符号

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 。如果不计算变量的地址,这个值就需要是已载入的值,通过使用

movq x1@dtpoff (%rax), %r11

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

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

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

4.2.7. s390 局部动态 TLS 模式

对于 s390 ,如果仅访问一个变量,局部动态模式的代码序列,对于常规动态模式,没有优势。它甚至会稍微更糟糕一些,因为需要载入一个额外的字常数库( literal pool )的项( x@tlsldm x@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@dtpoff x2@dtpoff 被汇编器翻译为重定位 R_390_TSL_LDO32 。链接器将为该模块计算 TLS 块中 x1 x2 的偏移,并写入字常数库。

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

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

4.2.8. s390x 局部动态 TLS 模式

s390x 的局部动态访问模式类似于 s390 的版本。在 s390 s390s 的常规动态模式之间的差别依然存在。线程指针的提取要求 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 具有优势

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值