Thread 本機儲存空間

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_EL0x86_64,使用fs.base的區隔基準,且 你可以透過 %fs: 存取,並從 %fs:0rdfsbase 載入 指示
  • 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("...")))

哪個模型相關性包括:

  1. 哪個模組會執行存取權:
    1. 主要可執行檔
    2. 靜態 TLS 集中的模組
    3. 在啟動後載入的模組,例如製作者:dlopen
  2. 要存取的變數定義於哪個模組中:
    1. 在相同模組中 (例如 local-*)
    2. 在其他模組中 (例如 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_DTPMODR_X86_64_DTPOFF。在 AArch64 中 《R_AARCH64_DTPMOD》和《R_AARCH64_DTPOFF》。這些重新定位參照了符號 無論模組是否按照該名稱定義符號。

本地動態

局部動態與「全球動態」相同,但前者需要當地符號。可以 應視為單一 global-dynamic 存取,藉此存取此模組的 TLS 區塊。 然後因為模組中定義的每個變數都會與 TLS 區塊可將多個 global-dynamic 呼叫最佳化至單一呼叫。 編譯器會放寬 global-dynamic 存取 local-dynamic 的權限 您可以使用本機/靜態變數或隱藏瀏覽權限。連結器 有時候,或許可以放寬local-dynamicglobal-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> 是與 enda 的偏移值 主執行檔的傳輸層安全標準 (TLS) 部分。

連結器知道任何 XTPOFF_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 - $tpmax(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 較大,存取作業會使用更簡單的程式碼路徑 就這樣。