天天看点

ELF对线程局部储存的处理(2)4.        TLS 访问模式

4.        TLS 访问模式

文档到目前为止,提到了访问线程局部储存的两种不同的方式,动态模式及静态模式。 TLS 访问模式有基本的区分( basic differentiation )。不同的模式,归属于这两个分类之一,用于提供尽可能多好的性能。这个文档所涵盖的 ABI 定义了四种不同的访问模式。用于其它平台的 ABI 可能会定义额外的模式。

所有的模式的共同点是,在启动时刻的动态链接器,或当一个模块被动态加载时,需要处理与线程局部储存相关的重定位。这些重定位的处理不可以被推迟( processing of none of these relocations can be deferred );正如变量的其它重定位(而不是函数调用),它们必须被立刻处理。

当为 STT_TLS 符号执行一个重定位时,其结果是一个模块 ID 及一个 TLS 块偏移。对于普通符号的重定位,其结果将是该符号的地址。然后模块 ID 及 TLS 偏移被存入 GOT 中。代码段不能被修改,因而由编译器产生的代码不能修改,而链接器具有从 GOT 读取这些值的指令。

4.1. 常规动态TLS 模式

常规动态 TLS1 是最通用的。用它编译出来的代码可以被用在各处,它可以访问在其他地方定义的变量。编译器默认地将用这个模式产生代码,只有当被显式告知,或者可以安全使用其他模式而不损害通用性时,才会采用限制性更大的模式。

为这个模式产生的代码不对模块的数目做任何假设,也不假定在链接时刻已知变量偏移(先不谈编译时刻)。模块 ID 及 TLS 块偏移由动态链接器在运行时刻来确定,然后以架构特定的方式传递给 __tls_get_addr 函数。函数 __tls_get_addr 在返回时,已经计算出用于当前线程的变量地址。

实现这个模式的代码的大小,在运行时为重定位以及在代码中计算地址所需的时间,使得尽可能避免这个模式是必要的。如果模块 ID 及 TLS 块偏移,或甚至只有模块 ID ,已知,就有更好的可行的方法。

因为在这个模式中,函数 __tls_get_addr 被调用来计算变量地址,通过上面描述的技术来推迟 TLS 块的分配是可能的。如果链接器正在修改代码,使其更高效,这将是一个不允许延迟分配的模式。

在下面的章节中,所显示的代码确定一个线程局部变量 x 的一个地址:

extern __thread int x;

&x;

4.1.1. IA-64 常规动态 TLS 模式

因为 IA-64 版本的 __tls_get_addr 函数期望模块 ID 及 TLS 模块偏移作为参数,在 IA-64 中,常规动态 TLS 模式的代码序列,必须把这两个值载入参数寄存器 out0 及 out1 。结果将在结果寄存器 ret0 中。

着重指出 IA-64 ABI 不提供链接器放宽的条款。一旦为某个模式产生代码,链接器即便发现这个模式不是最佳的,它也无能为力。因此编译器(有时在程序员的指引下)产生合适的代码很重要。

在代码序列中,指令被分配偏移的地址。对于 IA-64 ,这仅有助于更方便地引用指令。编译器可以自由地重新安排它们。

常规动态模式代码序列 初始重定位                     符号

0x00 mov loc0=gp

0x06 addl [email protected](@dtpmod(x)), gp

0x0c addl [email protected](@dtprel(x)), gp

;;

0x10 ld8 out0=[t1]

0x16 ld8 out1=[t2]

0x1c br.callrp=__tls_get_addr

    ;;

0x20 mov gp=loc0

R_IA_64_LTOFF_DTPMOD22     x

R_IA_64_LTOFF_DTPREL22     x

GOT [m]

GOT [n]

未解决的重定位

R_IA_64_ DTPMOD64LSB       x

R_IA_64_ DTPREL64LSB       x

地址 0x06 的指令确定,为表达式 @ltoff (@dtpmod(x)) 产生的, GOT 项的地址。在指令中,链接器从 gp 寄存器置入该项的 22 位偏移,并创建一个新的 GOT 项,例子中的 GOT[m] ,它在运行时由动态链接器填写。为此,动态链接器必须要处理 R_IA_64_DTPMOD64LSB 类型的重定位,来确定包含符号 x 的模块 ID (在使用高位在前的平台上,重定位类型是 R_IA_64_DTPMOD64MSB )。

地址 0x0c 的指令得到类似的处理。汇编器通过保存指令中 gp 相关的 GOT 项的偏移,来处理 @ltoff (@dtprel(x)) 表达式,并分配了一个新的 GOT 项。动态链接器在运行时,在这个 GOT 项, GOT [n] (这里 n 和 m 没有任何关系),存入变量 x 在其所在模块的 TLS 块中的偏移。这个值通过处理附加到这个 GOT 项的,重定位类型 R_IA_64_DTPREL64LSB 来确定(在使用高位在前的平台上,这是 R_IA_64_DTPREL64MSB )。

余下的代码一目了然。 GOT 的值通过两个 ld8 指令载入,并保存入后面调用函数 __tls_get_addr 的参数寄存器中。在前面我们已经看到这个函数的原型,显然它与这里代码所使用的相符。

在返回时,计算出来的线程局部变量 x 的地址被保存在寄存器 ret0 里。

4.1.2. IA-32 常规动态 TLS 模式

IA-32 用于常规动态模式的代码序列存在两个版本,因为如上所述,对函数 __tls_get_addr 不同的调用。第一个版本遵循 Sun 的模型:

常规动态模式代码序列 初始重定位                     符号

0x00 leal [email protected] (%ebx), %edx

0x06 pushl %edx

0x07 call [email protected]

0x0c popl %edx

0x0d nop

R_386_TLS_GD_32            x

R_386_TLS_GD_PUSH         x

R_386_TLS_GD_CALL         x

R_386_TLS_GD_POP           x

GOT [n]

GOT [n+1]

未解决的重定位

R_386_TLS_DTPMOD32        x

R_386_TLS_DTPOFF32         x

IA-32 ABI 的函数 __tls_get_addr 仅接受一个包含信息的 tls_index 结构体地址的参数。为表达式 [email protected] (%ebx) 所构建的重定位类型 R_386_TLS_GD_32 ,指示链接器在 GOT 上分配这样的一个结构体。 tls_index 对象所要求的两个项当然是必须连续的(在例子代码中的 GOT[n] 及 GOT[n+1] )。这些 GOT 中的位置与重定位类型 R_386_TLS_DTPMOD32 及 R_386_TLS_DTPOFF32 关联。这两个 GOT 项的次序由 tls_info 定义中相应的域的次序决定。

地址 0x00 的指令,仅通过把在链接时刻已知的到 GOT 开头的偏移加上 GOT 寄存器 %ebx 的内容,计算出第一个 GOT 项的地址。结果被保存在任何可用的 32 位寄存器中。上面例子代码中使用 %edx 寄存器,不过链接器被假定能处理使用任意寄存器。然后这个地址通过栈被传递给 __tls_get_addr 。 pushl 及 popl 执行这个工作。它们有自己的重定位,因此在随后可能出现的代码放宽的情况下,链接器能够识别这些指令。

表达式 [email protected] 是对 __tls_get_addr 的调用。在这里不能简单地写做 call [email protected] ,因为没有向汇编器提供相关联符号的信息(这里是 x ),因而不能构建正确的重定位。这个重定位,再次的,对于可能出现的代码放宽是必要的。

在调用这个函数之后,寄存器 %eax 包含了线程局部变量 x 的地址。地址 0x0d 的 nop 指令添加在这里,以产生一个允许执行代码放宽的代码序列。正如我们将要看到的,其它访问模式用到的某些代码序列需要更多空操作。

GNU 版本的代码序列是相似的,但大大地简化了:

常规动态模式代码序列 初始重定位                     符号

0x00 leal [email protected] (, %ebx, 1), %eax

0x07 call [email protected]

R_386_TLS_GD                 x

R_386_PLT32               ___tls_get_addr

GOT [n]

GOT [n+1]

未解决的重定位

R_386_TLS_DTPMOD32        x

R_386_TLS_DTPOFF32         x

用于 ___tls_get_addr 的调用规范把代码序列减少到 2 条指令。参数在 %eax 寄存器中传给函数。这正是 0x00 地址指令所做的事情。为了暗示这个指令用在 GNU 版本的访问模式,使用了语法 [email protected] (%ebx) 。这创建了重定位 R_386_TLS_GD ,而不是 R_386_TLS_GD_32 。这对 GOT 的影响是相同的。链接器在 GOT 上分配 2 个槽,由 GOT 寄存器 %ebx 放入相对于 GOT 的偏移。注意到 leal 第一个操作数的形式,强制使用这个指令的 SIB 形式,把指令的大小增加了 1 字节,避免了额外的 nop 。

调用指令也不同。这里不需要一个特殊的重定位,因此使用普通的函数调用语法调用 ___tls_get_addr 。

4.1.3. SPARC 常规动态 TLS 模式

SPARC 常规动态访问模式非常类似于 IA-32 。函数 __tls_get_addr 以一个指向 tls_index 类型对象的指针参数来调用。

常规动态模式代码序列 初始重定位                     符号

0x00 sethi %hi (@dtlndx (x)), %o0

0x04 add %o0, %lo (@dtlndx (x)), %o0

0x08 add %l7, %o0, %o0

0x0c call [email protected]

R_SPARC_TLS_GD_HI22          x

R_SPARC_TLS_GD_LO10         x

R_SPARC_TLS_GD_ADD           x

R_SPARC_TLS_GD_CALL         x

GOT [n]

GOT [n+1]

GOT [n]

GOT [n+1]

未解决的重定位, 32 位

R_SPARC_TLS_DTPMOD32        x

R_SPARC_TLS_DTPOFF32         x

未解决的重定位, 64 位

R_SPARC_TLS_DTPMOD64        x

R_SPARC_TLS_DTPOFF64         x

表达式 @dtlndx (x) 使得链接器在 GOT 中创建 tls_info 类型的对象。归究于 SPARC 的 RISC 架构,偏移不得不在寄存器 %o0 中以两步载入。使用 %hi () ,表达式 @dtlndx (x) 产生了一个 R_SPARC_TLS_GD_HI22 重定位;而下一条指令使用 %lo () 来得到低 10 位,这样产生了相匹配的 R_SPARC_TLS_GD_LO10 重定位。

载入的偏移是,当构建一个可执行或共享对象时,链接器将要加入的,在 GOT 中的 2 个连续字的第一个字的偏移。并且它们分别被赋予了重定位 R_SPARC_TLS_DTPMOD64 及 R_SPARC_TLS_DTPOFF64 (译:仅对于 64 位而言)。这些重定位将指示动态链接器查找线程局部变量 x ,并在第一个字中保存所在模块的 ID ,在第二个字中保存 TLS 块的偏移。

地址 0x08 的 add 指令产生最终的地址。在这里例子中,寄存器 %l7 被预计包含 GOT 的指针。不过链接器可以处理任意寄存器,不只是 %l7 。唯一的要求是 GOT 寄存器必须是指令中的第一个寄存器。为了定位该指令,一个 R_SPARC_TLS_GD_ADD 重定位被加入这个指令中。

序列中最后一条指令是对 __tls_get_addr 的调用,它导致加入了重定位 R_SPARC_TLS_GD_CALL 。

代码序列不可以修改。不能把第二个 add 指令移到 call 指令的延迟槽( delay slot )里,因为链接器将不能识别这个指令序列。 [1]

4.1.4. SH 常规动态 TLS 模式

在常规动态模式中访问一个 TLS 变量,只是简单地访问一个全局变量的代码及一个函数调用的串接。这个全局变量包含了 TLS 变量地址的偏移,这个值由链接器来决定。被调用的函数是 __tls_get_addr 。

常规动态模式代码序列 初始重定位                     符号

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 [email protected]

2:    .long [email protected]

3:

R_SH_TLS_GD_32                x

GOT [n]

GOT [n+1]

未解决的重定位

R_SH_TLS_DTPMOD32         x

R_SH_TLS_DTPOFF32         x

保存在带有标记 1: 的字中的值包含了,构成 tls_index 对象的两个 GOT 项中,第一个的链接时的偏移常量。这个对象的完整地址将被在 0x0a 的指令计算出来。第二及第三条指令通过通常的代码序列算出 __tls_get_addr 的地址。然后在地址 0x08 的指令中,调用这个函数,它把结果在 r0 中返回。注意到地址 0x0a 的 add 指令在跳转的延迟槽中执行。在 __tls_get_addr 返回后,所要做的,就是跳过数据。

值得指出的是,这个代码的代价相当高。在常规动态模式中,每次访问一个 TLS 变量要求四个字的数据,及 2 条额外指令来跳过这些放在代码段中间的数据。

4.1.5. Alpha 常规动态 TLS 模式

Alpha 常规动态 TLS 模式与 IA-32 的类似。函数 __tls_get_addr 通过一个指向 tls_index 类型的对象的指针参数来调用。

常规动态模式代码序列 初始重定位                     符号

0x00  lda  $16, x($gp)   !tlsgd!1

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

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

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

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

R_ALPHA_TLSGD              x

R_ALPHA_LITERAL     __tls_get_addr

R_ALPHA_LITUSE              4

R_ALPHA_GPDISP              4

GOT [n]

GOT [n+1]

未解决的重定位

R_ALPHA_DTPMOD64        x

R_ALPAH_DTPREL64         x

重定位指示符 !tlsgd 使得链接器在 GOT 中创建 tls_info 类型的对象。这个对象的地址通过 lda 指令被载入第一个实参寄存器 $16 。余下的序列是一个函数的标准调用序列,除了使用 !lituse_tlsgd 而不是 !lituse_jsr 。在讨论放宽( relaxation )时,其原因显而易见。

4.1.6. x86-64 常规动态 TLS 模式

x86-64 常规动态 TLS 模式与 GNU 版本的 IA-32 的类似。函数 __tls_get_addr 通过一个指向 tls_index 类型的对象的指针参数来调用。

常规动态模式代码序列 初始重定位                     符号

0x00  .byte   0x66

0x01  leaq     [email protected] (%rip), %rdi

0x08  .word  0x6666

0x0a  rex64

0x0b  call [email protected]

R_X86_64_TLSGD                x

R_X86_64_PLT32        __tls_get_addr

GOT [n]

GOT [n+1]

未解决的重定位

R_X86_64_DTPMOD64        x

R_X86_64_DTPOFF64         x

X86-64 ABI 的函数 __tls_get_addr 仅接受一个参数,它是包含相关信息的 tls_index 结构体的地址。为表达式 [email protected] (%rip) 构建的重定位 R_X86_64_TLSGD ,指示链接器在 GOT 中分配这样的一个结构体。 tls_index 对象所要求的两个项当然是必须连续的(在例子代码中的 GOT[n] 及 GOT[n+1] )。这些 GOT 中的位置分别与重定位类型 R_X86_64_DTPMOD64 及 R_X86_64_DTPOFF64 关联。

地址 0x00 处的指令仅是通过,把在链接时刻已知的 GOT 头与 PC 的相对地址,加上当前指令指针,来计算第一个 GOT 项的地址。结果通过 %rdi 寄存器传递给函数 __tls_get_addr 。注意到这个指令必须有一个前导的 data16 ,并且后面紧跟着 0x08 的 call 指令(译:应该是 0xa )。而 call 指令有两个前导的 data16 及一个前导的 rex64 ,以把整个代码序列扩展到 16 字节。使用前缀而不是 no-op 指令,是因为前者对代码没有负面的影响。

4.1.7. s390 常规动态 TLS 模式

对于 s390 常规动态 TLS 模式,在调用 __tls_get_offset 之前,编译器必须设置 GOT 寄存器 %r12 。函数 __tls_get_offset 接受一个参数,它是到一个 tls_index 类型对象的 GOT 偏移。该函数调用的返回值必须被加到线程指针上,来获得所要求的变量的地址。

常规动态模式代码序列 初始重定位                     符号

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

ear %r7, %a

R_390_TLS_GDCALL               x

R_390_TLS_GD32                  x

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

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

la  %r8, 0 (%r2, %r7) # %r8 = &x

...

.L0 : # literal pool, address in %r13

.L1: .long [email protected]

.L2: .long [email protected]

GOT [n]

GOT [n+1]

未解决的重定位

R_390_TLS_DTPMOD               x

R_390_TLS_DTPOFF                 x

为字常数库的项 [email protected] 所构建的重定位 R_390_TLS_GD32 ,指示链接器在 GOT 中分配一个占据两个连续 GOT 项的 tls_index 结构体。这两个 GOT 项分别关联着重定位 R_390_TLS_DTPMOD 及 R_390_TLS_DTPOFF 。

调用 __tls_get_offset 的指令标记了重定位 R_390_TLS_GDCALL 。这个指令受 TLS 模式优化的支配。这个标记是必要的,因为链接器需要知道调用的位置,这样可以使用一个不同 TLS 模式的指令来替换它。这个指令标记在汇编器语法中如何指定,由汇编器的实现来决定。

指令序列被分为四个部分。第一部分从 %a0 获取线程指针,并载入到 __tls_get_offset 的跳转偏移。这第一部分可以被其它 TLS 访问所重用。第二次的 TLS 访问不需要重复这两条指令,如果在这两次 TLS 访问期间, %r6 及 %r7 没有破坏,就可以使用它们。第二部分是这个 TLS 访问的核心。对于通过常规动态访问模式所访问的每个变量而言,这两条指令都要出现。第一条指令向来自字常数库( literal pool )的变量 tls_index 载入 GOT 偏移,第二条指令调用 __tls_get_offset 。第三部分使用提取入 %r7 的线程指针,及由 __tls_get_offset 调用在 %r2 中返回的偏移,在变量上执行一个操作。在例子中, x 的地址被载入寄存器 %r8. 编译器可以选择任意其他合适的指令来访问 x ,例如“ l %r8, 0 (%r2, %r7) ”将把 x 的内容载入 %r8 。这给编译器留下的优化余地。第四部分是字常数库,它必需要有一个项对应 [email protected] 偏移。

S390 常规动态访问模式的所有指令都可以被编译器自由地安排,只要满足明显的数据依赖,并且寄存器 %r0 - %r5 不包含任何在 bas 指令后还需要的信息(它们被函数调用破坏)。寄存器 %r6 , %r7 及 %r8 都不是固定使用的,它们可以被任何其它合适的寄存器所代替。

4.1.8. s390x 常规动态 TLS 模式

s390x 常规动态访问模式或多或少是 s390 常规动态模式的一个拷贝。主要的差别在于提取线程指针的代码更复杂,使用 bras1 指令而不是 bas 指令,另外,事实上 s390x 使用 64 位的偏移。

常规动态模式代码序列 初始重定位                      符号

ear  %r7, %a0

sllg  %r7, 32

ear  %r7, %a1

R_390_TLS_GDCALL               x

R_390_TLS_GD64                  x

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

bras1 %r14, [email protected]

la  %r8, 0 (%r2, %r7) # %r8 = &x

...

.L0 : # literal pool, address in %r13

.L2: .quad [email protected]

GOT [n]

GOT [n+1]

未解决的重定位

R_390_TLS_DTPMOD               x

R_390_TLS_DTPOFF                 x

重定位 R_390_TLS_GD64 , R_390_TLS_DTPMOD 及 R_390_TLS_DTPOFF 的作用与它们的 s390 的对手一样,只是重定位的目标是 64 位,而不是 32 位。

[1] 至少 Sun 的文档是这么说,显然 Sun 的链接器也这么做。给定了显示了指令实际做什么的重定位,这样做似乎不是真正必要。

继续阅读