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

5.        链接器优化

线程局部储存访问模式,就使用的方式而言,是分层的。最通用的模式是常规动态模式,它可以随处使用。当生成执行映像本身时,初始可执行模式可以无条件使用。如果一个共享对象不是要动态载入的,它也可以使用。这两个模式已经定义了一个层次。余下两个模式,如果定义是与引用在同一个模块中,是对更通用模式之一的特殊优化。访问模式之间的层次及转换可以图形化地显示如下 [1]



[1] 这个图一开始是由 Mike Walker 制作的

体

 

这个图显示了访问线程局部变量的一个代码序列可以(或不可以)被编译器及链接器优化。实线表示从任意位置出发的默认路径。缺省方式总是不改变代码。优化由虚线表示。

优化可以来自五个原因:

Ÿ           程序员告诉编译器为一个执行映像产生代码,而不是共享对象。

Ÿ           程序员告诉编译器产生的代码不会直接访问动态加载的变量(使用 dlsym 就可以了)。

Ÿ           编译器认识到一个线程局部变量是受保护的。即,引用与定义在同一个模块中。

Ÿ           链接器知道创建的是一个执行映像还是共享对象。执行映像(类型 ET_EXEC )遵从第一个分类,像位置无关执行映像( Position Independent Executable )那样。

Ÿ           链接器知道,在执行映像代码中,对线程局部变量的引用,是否无条件地,被出现在执行映像中的定义所满足。定义不需要受保护,因为执行映像总是符号查找路径上出现的第一个对象。

在对架构的访问模式的描述中,我们已经解释了使用这些模式的先决条件。在下面的章节里,我们详细解释代码放宽如何发生。这不是无足轻重的,因为在代码序列中所使用的指令之间没有 1 1 的关系,我们必须处理这些差异。

在这个文档目前所定义的架构中,只有 IA-32 SPARC x86-64 Alpha SH 定义了链接器优化。为 IA-64 做这个优化,至少可以说,非常困难。为 IA-64 产生的代码,理想地,要把代码序列中的代码束( bundle )尽可能地分开,以提高并行度。但这意味着定位归属在一起的指令就是要做的一切,但琐碎。即便使用重定位标记指令也不能奏效,因为多个代码序列可以被合并起来,一次载入或保存多个线程局部变量。只有非常复杂的流分析( flow analysis )可以找出单个代码序列( individual code sequences ),而目前还没有这样的计划。

定义了优化的架构要求编译器按要求发布代码。这,连同标记指令的重定位,将允许链接器识别代码序列。小的改动像使用不同的寄存器,可以很容易地被屏蔽掉。代码序列如何被识别的细节将不在这里讨论。我们假定链接器有这个能力,集中关注这里完成的实际工作。

5.1. IA-32 链接器优化

这个链接器可以执行 4 种不同的优化,它们通过减少运行时重定位及从内存载入,来节省执行时间。图只显示了 3 个转换,不过初始可执行到局部可执行的转换是可以执行的。因为 Sun GNU 版本的代码序列不相同,这里我们需要分别讨论它们。

概括某些优化的副作用。如果原始代码使用常规动态或局部动态访问模式,使用函数 __tls_get_addr 来访问变量。如果只是使用了这两个模式,这意味着 TLS 块的分配可以被推迟,就像前面章节解释的那样。如果链接器执行优化,对 TLS 块不使用 __tls_get_addr 的访问,就有机会最终分配,静态模式自动启用并且必须设置 DF_STATIC_TLS 标记的内存。这通常不是个障碍,因为对静态 TLS 块的访问是频繁的,而对动态加载代码,延迟分配非常有用。

常规动态到初始可执行

链接器能执行的最重要的放宽,可能是从常规动态到初始可执行模式。常规动态模式是运行时代价最高的,因此应该尽量避免。首先,我们处理 Sun 版本。

 

GD à IE 代码转换

初始重定位                     符号

0x00  leal    x@dtlndx (%ebx), %edx

0x06  pushl  %edx

0x07  call   x@TLSPLT

0x0c  popl   %edx

0x0d  nop

            

0x00  movl  x@tpoff (%ebx), %edx

0x06  movl  %gs:0, %eax

0x0c  subl   %edx, %edx

R_386_TLS_GD_32                x

R_386_TLS_GD_PUSH            x

R_386_TLS_GD_CALL           x

R_386_TLS_GD_POP             x

 

                         

R_386_TLS_IE_32               x

 

GOT [n]

未解决的重定位

R_386_TLS_TPOFF32            x

 

每当创建一个可执行映像时,就可以执行这个优化。这个优化对于 GNU 版本是类似的。

 

GD à IE 代码转换

初始重定位                     符号

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

0x07  call  __tls_get_addr@plt

             

0x00  movl  %gs:0, %eax

0x06  addl  x@gotntpoff (%ebx), %eax

R_386_TLS_GD                 x

R_386_PLT32             __tls_get_addr

                     

 

R_386_TLS_GOTIE               x

 

GOT [n]

未解决的重定位

R_386_TLS_TPOFF              x

 

现在可以清楚了,为什么两个版本的常规动态模式的代码序列都比需要的要长。在 Sun 的案例中的 nop ,及 GNU 版本中的 SIB 形式都是需要的,为 IE 代码序列留下余地。

常规动态到局部可执行

ELF 的符号查找规则定义,如果一个在可执行映像中用到的符号,被定义在这个可执行映像中,它总是会被选上的。原因是这个可执行映像总是在查找域链表的头部。因此,常规动态到局部动态模式也是相当常见的,这个模式甚至比到初始可执行模式还要廉价。

 

GD à LE 代码转换

初始重定位                     符号

0x00  leal    x@dtlndx (%ebx), %edx

0x06  pushl  %edx

0x07  call   x@TLSPLT

0x0c  popl   %edx

0x0d  nop

            

0x00  movl  x@tpoff (%ebx), %edx

0x05  nop

0x06  movl  %gs:0, %eax

0x0c  subl   %edx, %edx

R_386_TLS_GD_32              x

R_386_TLS_GD_PUSH            x

R_386_TLS_GD_CALL           x

R_386_TLS_GD_POP             x

 

                         

R_386_TLS_LE_32               x

 

未解决的重定位

 

对于 Sun 版本,这个优化减少了一条指令,并使用一个内存载入及一个算术运算替代了函数调用。 GNU 的版本同样给力:

 

GD à LE 代码转换

初始重定位                     符号

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

0x07  call  __tls_get_addr@plt

             

0x00  movl  %gs:0, %eax

0x06  addl  x@ntpoff, %eax

R_386_TLS_GD                 x

R_386_PLT32             __tls_get_addr

                      

 

R_386_TLS_LE              x

 

未解决的重定位

 

请注意在替代的代码中 movl 指令的长度。它假定使用了 mod R/M 个字节。

局部动态到局部可执行

如果用户没有告诉编译器,代码是用于一个可执行映像的目的,链接器仍然存在优化代码的可能,不过就像下面所见的,结果不是很理想。

 

LD à LE 代码转换

初始重定位                     符号

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

0x06  pushl %edx

0x07  call  x1@TLSPLT

0x0c  popl  %edx

0x10  movl $x1@dtpoff, %edx

0x15  addl %eax, %edx

0x00  movl %gs:0, %eax

0x06  nop

0x07  nop

0x08  nop

0x09  nop

0x0a  nop

0x0b  nop

0x0c  nop

      …

0x10  movl $x1@tpoff, %eax

0x15  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_LE_32                 x1

 

未解决的重定位

 

一长串的 nop 是为局部动态模式所产生的大的代码序列导致的。在这里它无法避免。只有程序员告诉编译器,代码是用于可执行映像的,才可以避免它。这里描述的来自 Sun 提供的文献。 GNU 版本有同样的问题,不过解决了它,以减小对运行时性能影响。

 

LD à LE 代码转换

初始重定位                     符号

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

0x07  call  __tls_get_addr@plt

      …

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

             

0x00  movl  %gs:0, %eax

0x06  nop

0x07  leal   0x0 (%esi, 1), %esi

      …

0x10  leal  x@ntpoff (%eax), %edx

R_386_TLS_LDM                x1

R_386_PLT32             __tls_get_addr

 

R_386_TLS_LDO_32             x1

                     

 

 

 

 

R_386_TLS_LE                  x1

 

未解决的重定位

 

在地址 0x07 的指令需要一些解释。它可能看上去像某些做很多事情代价比较高的指令,但事实上它是一个 no-op 。寄存器 %esi 的值,在乘以 1 、加上 0 后,保存回相同的寄存器。选择这条指令的原因是,它是长指令,确切地 4 字节。这意味着填满 5 字节的空洞,我们只需要再多一条 nop 指令。这比使用 7 nop 指令(类似 Sun 的做法)要划算得多。

在局部动态模式不是计算地址,而是直接载入或保存变量的情况下,转换后的代码也同样简单地载入或保存。这个转换是简单的,就像上面代码所显示的:通过把重定位 R_386_TLS_LDO_32 改成 R_386_TLS_LE-32 实现 -x1@tpoff (%eax) ,并使用它替换 x1@dtpoff (%eax)

初始可执行到局部可执行

如果代码被编译只在一个可执行映像中使用,并且变量在这个可执行映像中有效( available ),这个最后的优化帮助榨出最后一分性能。这个转换,与局部动态到局部可执行的转换比较,远没有那么挥霍。

 

IE à LE 代码转换

初始重定位                     符号

0x00  movl  x@tpoff (%ebx), %edx

0x06  movl  %gs:0, %eax

0x0c  subl   %edx, %edx

            

0x00  movl  $x@tpoff, %edx

0x05  nop

0x06  movl  %gs:0, %eax

0x0c  subl   %edx, %eax

R_386_TLS_IE_32               x

 

 

R_386_TLS_LE_32               x

 

未解决的重定位

 

这个优化节省了一个运行时重定位,把一个内存载入转换为载入一个立即数,不过也加入了一个新指令。这个指令是 nop ,它不会对执行带来什么影响。 GNU 版本则不需要那么难看。

 

IE à LE 代码转换

初始重定位                     符号

0x00  movl  %gs:0, %eax

0x06  addl  x@gotntpoff (%ebx), %eax

             

0x00  movl  %gs:0, %eax

0x06  leal   x@ntpoff (%eax), %eax

 

R_386_TLS_GOTIE               x

                  

 

R_386_TLS_LE                  x

 

未解决的重定位

 

5.2. SPARC 链接器优化

因为用于 SPARC 的模式基本上等同于 IA-32 ,毫不奇怪,这里同样有相同的 4 种优化。一般而言,归究于 SPARC 处理器的 RISC 指令集,这里的优化要清晰一些,不像 IA-32 不统一的 CISC 指令长度。

常规动态到初始可执行

这个优化设法去掉一个运行时重定位,以及对 __tls_get_addr 函数的调用。但是用于静态 TLS 块的内存分配不能推迟了,并且必须设置 DF_STATIC_TLS 标记。

 

GD à IE 代码转换

初始重定位                     符号

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

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

0x08 add %l7, %o0, %o0

0x0c call __tls_get_addr@plt

        

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

0x04 or   %o0, %lo (@tpoff (x)), %o0

0x08 ld   [%l7+%o0], %o0

0x0c add  %g7, %o0, %o0

R_SPARC_TLS_GD_HI22          x

R_SPARC_TLS_GD_LO10         x

R_SPARC_TLS_GD_ADD          x

R_SPARC_TLS_GD_CALL         x

                           

R_SPARC_TLS_IE_HI22           x

R_SPARC_TLS_IE_LO10           x

 

GOT [n]

未解决的重定位

R_SPARC_TLS_DTPOFF32         x

 

在这里我们不不列出 64 位版本。其中的区别与 4.3.3 节描述的一样。实际用于 GOT 指针的寄存器(上面代码中是 %l7 )可以不同。链接器将,从标记着 R_SPARC_TLS_GD_ADD 的指令,了解使用的寄存器。

常规动态到局部可执行

这个优化亦很直观,常规动态模式的指令为局部可执行模式的指令所替代。唯一需要记住的事是,使用一个 nop 填充短的局部可执行代码。

 

GD à LE 代码转换

初始重定位                     符号

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

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

0x08 add %l7, %o0, %o0

0x0c call __tls_get_addr@plt

        

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

0x04 xor  %o0, %lox (@tpoff (x)), %o0

0x08 add  %g7, %o0, %o0

0x0c nop

R_SPARC_TLS_GD_HI22          x

R_SPARC_TLS_GD_LO10         x

R_SPARC_TLS_GD_ADD          x

R_SPARC_TLS_GD_CALL         x

                           

R_SPARC_TLS_IE_HIX22          x

R_SPARC_TLS_IE_LOX10          x

 

未解决的重定位

 

这个优化移除了两个运行时重定位,以及对 __tls_get_addr 的调用。不过,可执行映像必须设置标记 DF_STATIC_TLS

局部动态到局部可执行

局部动态到局部可执行模式的转换,在 SPARC 上,也是最不理想的。最好是许可( enable )编译器马上产生最适宜的代码。不过,不管怎样,优化还是有效的,因为它消除了一个运行时重定位及对 __tls_get_addr 的调用。

 

LD à LE 代码转换

初始重定位                     符号

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 x0r  %l1, %lox (@dtpoff (x1)), %l1

0x18 add  %o0, %l1, %l1

        

0x00 nop

0x04 nop

0x08 nop

0x0c mov %g0, %o0

     ...

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

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

0x18 add  %o0, %l1, %l1

R_SPARC_TLS_LDM_HI22         x1

R_SPARC_TLS_LDD_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_LE_HIX22          x1

R_SPARC_TLS_LE_LOX10          x1

 

未解决的重定位

 

这个优化同样要求可执行映像设置 DF_STATIC_TLS 标记。

初始可执行到局部可执行

如果程序员告诉编译器代码只用于可执行映像,但只有链接器知道变量被定义在这个可执行映像中,下面的优化可以帮助消除剩下的运行时重定位。

 

IE à LE 代码转换

初始重定位                     符号

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

0x04 or    %o0, %lo (@tpoff (x)), %o0

0x08 ld    [%l7 + %o0], %o0

0x0c add  %g7, %o0, %o0

            

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

0x04 xor   %o0, %lo (@tpoff (x)), %o0

0x08 mov  %o0, %o0

0x0c add   %g7, %o0, %o0

R_SPARC_TLS_IE_HI22            x

R_SPARC_TLS_IE_LO10           x

R_SPARC_TLS_IE_LD             x

R_SPARC_TLS_IE_ADD            x

                   

R_SPARC_TLS_IE_HIX22           x

R_SPARC_TLS_IE_LOX10          x

 

未解决的重定位

 

因为局部可执行模式的代码序列只有 3 条指令,在 0x08 的指令作为 no-op 加入。

5.3. SH 链接器优化

正如 IA-32 SPARC 那样,这个链接器可以执行一系列优化。但由于 SH 的代码结构及所使用的代码序列,可提供的选择有限。

常规动态到初始可执行

如果可以使用初始可执行模式,使用常规动态模式编译的代码,通过执行下面的转换,可以节省 2 条指令,及可能一个 GOT 项。

 

GD à IE 代码转换

初始重定位                     符号

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:

        

0x00  mov.l 1f, r0

0x02  stc   gbr, r4

0x04  mov.l @(r0, r2), r0

0x06  bra   3f

0x08  add   r4, r0

0x0a  nop

0x0c  nop

0x0e  nop

      .align 2

1:    .long x@gottpoff

2:    .long 0

3:

 

 

 

 

 

 

 

 

 

R_SH_TLS_GD_32                x

 

 

                   

 

 

 

 

 

 

 

 

 

 

R_SH_TLS_IE_32                 x

 

GOT [n]

未解决的重定位

R_SH_TLS_TPOFF32         x

 

__tls_get_addr 的调用被优化掉了,并且与跳转相关的指令及数据结构都没有了。注意到我们可以移动 bra 指令,这样用 nop 填充的,不需要的内存,不会再执行。

常规动态到局部可执行

从常规动态到局部可执行模式的转换与前一个转换几乎相同。我们只多节省了一条指令,并且没有留下运行时重定位。

 

GD à LE 代码转换

初始重定位                     符号

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:

        

0x00  mov.l 1f, r0

0x02  stc   gbr, r4

0x04  mov.l @(r0, r2), r0

0x06  bra   3f

0x08  nop

0x0a  nop

0x0c  nop

0x0e  nop

      .align 2

1:    .long x@tpoff

2:    .long 0

3:

 

 

 

 

 

 

 

 

 

R_SH_TLS_GD_32                x

 

 

                   

 

 

 

 

 

 

 

 

 

 

R_SH_TLS_LE_32                 x

 

未解决的重定位

 

再一次,可以巧妙地放置 bra 指令,来避免执行,所有用于填充目的的 nop 指令。

局部动态到局部可执行

这个最后的优化允许转换局部动态代码到局部可执行代码序列。以这个方式产生的代码,比上面描述的局部可执行代码序列,有更高效的潜力,因为线程寄存器只读一次。

 

LD à LE 代码转换

初始重定位                     符号

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

           

0x00  bra  3f

0x02  stc  gbr, r0

0x04  nop

0x06  nop

0x08  nop

0x0a  nop

0x0c  nop

0x0e  nop

      .align 2

1:    .long 0

2:    .long 0

3:    ...

     mov.l .Lp, r1

     mov.l r0, r1

     …

     mov.l .Lq, r1

     mov.l r0, r1

     …

.Lp:  .long x1@tpoff

.Lq:  .long x2@tpoff

 

 

 

 

 

 

 

 

 

R_SH_TLS_LD_32                x1

 

 

 

 

 

 

 

 

R_SH_TLS_LDO_32               x1

R_SH_TLS_LDO_32               x2

                          

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

R_SH_TLS_LE_32                 x1

R_SH_TLS_LE_32                 x2

 

未解决的重定位

 

因为计算用到的重定位地址现在非常简单(只是载入线程寄存器),序言( prologue )也同样简单。在跳转到第一个数据的延迟槽中可以执行一条指令。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值