ELF 執行緒本機儲存空間 ABI (TLS) 是儲存模型的變數儲存模型
每個執行緒都有專屬的全域變數副本這個模型
用於實作 C++ 的 thread_local
儲存模型。建立執行緒時
變數會從初始 TLS 映像檔提供初始值。TLS
變數都很適合做為執行緒安全程式碼中的緩衝區,
討論串簿保留也會處理 errno 或 dlerror 等 C 樣式錯誤
TLS 變數與任何其他全域/靜態變數相似。實作中
其初始資料會在「PT_TLS
」區隔中勝出。PT_TLS
區隔
位於唯讀 PT_LOAD
區段內,即使 TLS 變數可寫入。
接著,將這個區隔複製到不重複執行緒中,每個執行緒的處理程序
可寫入位置系統會影響「PT_TLS
」區隔的複製目標位置
以確保 TLS 變數的一致性
尊重。
ABI
編譯器、連結器和動態連接器必須遵循的實際介面 其實非常簡單 複雜的編譯器和連結器必須發出程式碼和動態重新定位, 請使用 4 種存取模式的其中一種 (如下一節所述)。動態 接著,連接器和執行緒實作程序必須完成所有設定, 以及實際可行的。不同架構有不同的 ABI,但相似 來聊聊大部分的雜訊 只需要一個 ABI本文件假設 x86-64 或 AArch64 會 並指出這些差異
TLS ABI 會使用幾個詞彙:
- Thread 指標:這是每個執行緒中唯一的位址,通常儲存位置
暫存器。執行緒本機變數位於與執行緒指標之間的偏移。
討論串指標會在本文件中以縮寫方式呈現,並做為
$tp
使用。$tp
是 AArch64 上的__builtin_thread_pointer()
傳回的內容在 AArch64 上$tp
是名為TPIDR_EL0
的特殊暫存器,可透過以下指令存取mrs <reg>, TPIDR_EL0
。x86_64
,使用fs.base
的區隔基準,且 你可以透過%fs:
存取,並從%fs:0
或rdfsbase
載入 指示 - TLS 區隔:各個模組中的資料圖片,由
每個模組的
PT_TLS
節目標題。並非所有模組都有PT_TLS
程式標頭,因此不是每個模組都有 TLS 區段。每個單元 最多包含一個 TLS 區段,且對應最多一個PT_TLS
節目標題。 - 靜態 TLS 集:此為
程式啟動時的動態連結器。這是由主要執行檔
以及
DT_NEEDED
(轉換提及) 的所有程式庫。這些模組 要求所設的DF_STATIC_TLS
DT_FLAGS
項目加入動態資料表中 (根據PT_DYNAMIC
區隔)。 - TLS 區域:此為各個專屬記憶體的連續區域
。
$tp
會指向這個區域中的某個點。這個 SDK 包含 Static TLS 集合中每個模組的 TLS 區段,以及 實作私人資料,有時也稱為 TCB (Thread) 「控制封鎖」)。AArch64 自$tp
起,16 位元組保留空間為 有時也稱為 TCB我們將這個空間稱為「ABI TCB」 本文介紹的功能 - TLS 封鎖功能:此為個別執行緒的 TLS 區塊副本。另有 每個執行緒一個 TLS 區塊。
- 模組 ID:模組 ID 並非以靜態方式知道,除了主 執行檔的模組 ID 一律為 1。其他模組的 ID 是 由動態連結器選擇每個委刊項都不重複 後續課程我們將逐一介紹 預先訓練的 API、AutoML 和自訂訓練理論上可以是任何非零的 64 位元 例如雜湊等模組實際上 動態連結器所維護的元件
- 主要執行檔:這是包含起始位址的模組。
與其中一個存取模型以特殊方式處理一律允許
模組 ID 為 1。這是唯一可以使用固定偏移量的模組
透過以下所述的本地執行器模型,從
$tp
擷取要求。
為遵守 ABI 規定,您必須支援所有存取模式。
存取模型
ABI 指定了 4 種存取模型:
global-dynamic
local-dynamic
initial-exec
local-exec
以下是可用於 -ftls-model=...
和
__attribute__((tls_model("...")))
哪個模型相關性包括:
- 哪個模組會執行存取權:
- 主要可執行檔
- 靜態 TLS 集中的模組
- 在啟動後載入的模組,例如製作者:
dlopen
- 要存取的變數定義於哪個模組中:
- 在相同模組中 (例如
local-*
) - 在其他模組中 (例如
global-*
)
- 在相同模組中 (例如
global-dynamic
可從任何地方使用,適用於任何變數。local-dynamic
可供任何模組使用, 同一個模組中initial-exec
可供任何模組用於靜態定義的任何變數 已設定 TLS。local-exec
可供主要執行檔用於 主執行檔
全球動態
全域動態是最常見的存取格式。同時也是最慢的配速。
任何執行緒本機全域變數應可使用這個方法存取。這個
存取模式必須使用
其他模組 (請參閱「初始執行」一節中的例外狀況)。已定義的符號
不需要使用這個存取模型。主要執行檔
也避免使用這種存取模式如果系統預測
編譯與 -fPIC
是共用程式庫的常態。
這個存取模型的運作方式是呼叫動態連接器中定義的函式。
呼叫函式的方式有兩種,可以選擇透過 TLSDESC 或
__tls_get_addr
。
如果是 __tls_get_addr
,則會傳遞 GOT
項目組合。
與這個符號相關聯具體來說,系統會將指標傳遞至第一個
第二個項目則位於文字後方如果是指定符號 S
,則第一個
項目 (以 GOT_S[0]
表示) 必須包含模組 ID
已定義 S
。第二個項目 (代表 GOT_S[1]
) 必須包含偏移值
TLS 區塊,與 PT_TLS
區段中的符號偏移值相同
每個相關模組的一部分接著使用 S
計算指向 S
的指標
__tls_get_addr(GOT_S)
。__tls_get_addr
的實作方式將為
TLSDESC 是 global-dynamic
存取權 (和 local-dynamic
) 的替代 ABI
其中第一個 GOT
版位使用另一組 GOT
版位
包含函式指標第二項包含已定義的一些動態連接器
輔助資料。這樣一來,動態連結器就能選擇要使用
呼叫方法和呼叫
不論是哪一種情況,都必須透過特定的
和一組特定速率讓連結器得以辨識
並可能將存取行為放寬為 local-dynamic
存取模式。
(注意:以下段落包含編譯器如何維護的詳細資料 直到 ABI 的結尾如果您不在乎的話,請略過本段內容。)
必須發出呼叫,編譯器才能為此存取模型發出程式碼
對 __tls_get_addr
(由動態連結器定義) 以及對
符號的名稱。具體來說,編譯器會發送程式碼給
GOT 本身需要額外位置處理) __tls_get_addr(GOT_S)
。
連結器在產生 GOT 時,會發出兩個動態重新定位。發布位置:x86_64
分別是 R_X86_64_DTPMOD
和 R_X86_64_DTPOFF
。在 AArch64 中
《R_AARCH64_DTPMOD
》和《R_AARCH64_DTPOFF
》。這些重新定位參照了符號
無論模組是否按照該名稱定義符號。
本地動態
局部動態與「全球動態」相同,但前者需要當地符號。可以
應視為單一 global-dynamic
存取,藉此存取此模組的 TLS 區塊。
然後因為模組中定義的每個變數都會與
TLS 區塊可將多個 global-dynamic
呼叫最佳化至單一呼叫。
編譯器會放寬 global-dynamic
存取 local-dynamic
的權限
您可以使用本機/靜態變數或隱藏瀏覽權限。連結器
有時候,或許可以放寬local-dynamic
的global-dynamic
存取權限
以下舉例說明編譯器如何針對此項目發出程式碼 存取模式:
static thread_local char buf[buf_cap];
static thread_local size_t buf_size = 0;
while(*str && buf_size < buf_cap) {
buf[buf_size++] = *str++;
}
可能降至
// GOT_module[0] is the module ID of this module
// GOT_module[1] is just 0
// <X> denotes the offset of X in this module's TLS block
tls = __tls_get_addr(GOT_module)
while(*str && *(size_t*)(tls+<buf_size>) < buf_cap) {
(char*)(tls+<buf>)[*(size_t*)(tls+<buf_size>)++] = *str++;
}
如果這個程式碼使用全域動態,就必須發出至少 2 次呼叫,一次
取得 buf 和其他路徑的指標,藉此取得 buf_size
的指標。
初始執行項目
只要編譯器知道哪個模組知道
存取的符號已定義於 的初始集合中
而不是使用 dlopen
開啟這種存取模式通常
僅在主要執行檔存取預設值為全域符號的情況下,才會使用這個值
曝光率。這是因為編譯執行檔
編譯器知道任何產生的程式碼都會位於初始的可執行檔集中。如果
系統會編譯 DSO,讓執行緒本機存取作業採用這個模型,然後是 DSO
無法使用 dlopen
安全開啟。在效能上可以接受這點
重要的應用程式,以及即使已知二進位檔,
以 dl 開啟,例如 libc 的情況。以這種方式編譯/連結的模組
已設定 DF_STATIC_TLS
標記。
在不使用 -fPIC
的情況下進行編譯時,預設執行環境為初始執行。
編譯器不會針對這個存取呼叫 __tls_get_addr
,而是發出程式碼
模型此功能會使用單一 GOT 項目,用來表示 GOT_s
符號
s
,編譯器會發出重新定位,確保
extern thread_local int a;
extern thread_local int b;
int main() {
return a + b;
}
會降為如下
int main() {
return *(int*)($tp + GOT[a]) + *(int*)($tp + GOT[b]);
}
請注意,在 x86 架構中,GOT[s]
實際上會解析為負值
值。
當地高階主管
這是最快存取模式,只有在符號位於
第一個 TLS 區塊,也就是主要執行檔的 TLS 區塊。僅供參考
主要執行檔可以使用這個存取模式,因為任何共用程式庫無法
(而且通常也不需要) 瞭解應用程式是否正在存取主要伺服器中的項目
執行檔。連結器會將 initial-exec
放寬至 local-exec
。編譯器
如果沒有明確指示,請透過 -ftls-model
或
__attribute__((tls_model("...")))
,因為編譯器無法知道
目前的翻譯單位會連結至主要執行檔
共用媒體庫
此偏移值的計算方法略有不同 從架構到架構
範例程式碼:
static thread_local int a;
static thread_local int b;
int main() {
return a + b;
}
會降為
int main() {
return (int*)($tp+TPOFF_a) + (int*)($tp+TPOFF_b));
}
在 AArch64 TPOFF_a == max(16, p_align) + <a>
上,p_align
完全符合
主要執行檔 PT_TLS
區段的 p_align
欄位,<a>
欄位為
a
從主要執行檔的 TLS 區段開頭開始偏移。
在 x86_64
TPOFF_a == -<a>
上,<a>
是與 end 的 a
的偏移值
主執行檔的傳輸層安全標準 (TLS) 部分。
連結器知道任何 X
的 TPOFF_X
為何,並填入這項資訊
值。
實作
本節說明在 Fuchsia 上實作的實作方式。這個 表示這裡的寬闊筆觸在不同程式庫之間顯得很類似 包括 musl 和 glibc 的實作項目
實際實作上述各項功能後,還會產生幾件事。名稱
所謂的「DTV」(動態執行緒向量) (在本文件中稱為 dtv
),
則會依模組 ID 為 TLS 區塊建立索引。下圖顯示初始的
可執行檔集在 Fuchsia 實作中,我們實際儲存了
執行緒描述元結構中的許多中繼資訊
ABI TCB (如下圖中的 tcb
)。在實作中,我們會使用前 8 個位元組
指向 DTV從頭 tcb
指向 dtv
,如下所示:
如下圖所示,不過在延遲之後,這可能會改變
arm64:
*------------------------------------------------------------------------------*
| thread | tcb | X | tls1 | ... | tlsN | ... | tls_cnt | dtv[1] | ... | dtv[N] |
*------------------------------------------------------------------------------*
^ ^ ^ ^ ^
td tp dtv[1] dtv[n+1] dtv
這裡X
的尺寸min(16, tls_align) - 16
是最大值tls_align
靜態 TLS 集內所有載入的 TLS 區段之對齊方式。設定方式為
靜態連結器會解析 TPOFF_*
值。這個
已設定邊框間距,因此在必要時,$tp
會與主要對齊
執行檔 PT_TLS
區段的 p_align
值,那麼 tls1 - $tp
會
max(16, p_align)
。這確保始終擁有至少 16 位元組空間
適用的 ABI TCB (在上圖中表示 tcb
)。
x86:
*-----------------------------------------------------------------------------*
| tls_cnt | dtv[1] | ... | dtv[N] | ... | tlsN | ... | tls1 | tcb | thread |
*-----------------------------------------------------------------------------*
^ ^ ^ ^
dtv dtv[n+1] dtv[1] tp/td
此處的 td
代表「執行緒描述元指標」。這兩種方式
會指向執行緒描述元這一帶的細微之處在模糊不清
圖表是 tcb
實際上是執行緒描述元結構的成員
這兩種情況,但 AArch64 是最後一位成員,x86_64
是第一個
成員。
dlopen
這張圖片說明初始執行檔的處理方式,但
說明 dlopen
案例發生的情況。呼叫 __tls_get_addr
時
請先檢查 tls_cnt
是否為模組 ID (由 GOT_s[0]
提供)
) 位於 dtv
內。如果答案為「dtv[GOT_s[0]] + GOT_s[1]
」
但如果沒有比較複雜查看
dynlink.c 中的 __tls_get_new
。
在概要中,已為更大的 dtv
分配了足夠的大空間
,dlopen
。而是在具備足夠空間的情況下運作的系統不一致
一律存在於已分配的位置接著設定較大的空間
為正確的 dtv
。接著,tcb
會設定為指向這個新的較大 dtv
。未來
由於 tls_cnt
較大,存取作業會使用更簡單的程式碼路徑
就這樣。