天天看點

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 的連結器也這麼做。給定了顯示了指令實際做什麼的重定位,這樣做似乎不是真正必要。

繼續閱讀