1. TLS 基本概念
Thread-Local Storage (TLS) 是一种允许每个线程拥有其私有全局变量或静态变量副本的机制。
在 ELF 文件中,所有 TLS 变量都被组织在一个由 PT_TLS 程序头(Program Header)描述的段(Segment)中。这个段并不直接用于运行时的变量访问,而是作为镜像模板,用于在线程创建时初始化该线程的私有存储空间。
PT_TLS 段通常包含两个关键的节(Section):
-
.tdata: 包含已显式初始化的 TLS 变量。它在 ELF 文件中占据实际空间,并在线程启动时被拷贝到线程的 TLS 块中。
-
.tbss: 包含未初始化(或初始化为零)的 TLS 变量。它在 ELF 文件中不占用磁盘空间(仅记录长度),在运行时会被映射并清零。
2. TLS 的四种访问模型
根据变量定义的地点和访问发生的地点,TLS 访问分为四种模型(从高性能到高通用性排序):
Local Executable (LE):
-
场景:主程序访问其自身定义的 TLS 变量。
-
处理:偏移量在链接时已确定,CPU 通过线程指针(TP)加上固定的偏移量直接访问。
Initial Executable (IE):
-
场景:主程序访问静态链接库或加载时已载入的动态库中的 TLS 变量。
-
处理:加载器在程序启动时计算偏移,并填入 GOT 表。通过 TP + GOT 条目值访问。
Local Dynamic (LD):
-
场景:动态库内部访问其自身定义的 TLS 变量。
-
处理:通过调用 __tls_get_addr 获取当前模块的 TLS 基址,再加上模块内偏移。
General Dynamic (GD):
-
场景:从外部访问动态加载(如 dlopen)模块中的 TLS 变量。
-
处理:最通用的模型,必须通过 __tls_get_addr(module_id, offset) 获取地址。
3. Relink的 TLS 处理流程
Relink的默认实现只支持使用LD和GD这两种TLS模型的ELF文件的加载,但支持所有TLS相关的重定位。有关Relink的介绍可以看下面这篇文章:
(3 封私信) Relink:Rust ELF 加载器与 Runtime/JIT 链接器 - 知乎
https://zhuanlan.zhihu.com/p/25152394738
3.1 加载与__tls_get_addr的实现
在解析 ELF 文件阶段,加载器会识别 PT_TLS 段,如果存在TLS段,加载器会注册TLS段的相关信息到全局中,并获得一个全局唯一的ModuleID,以便任何线程在需要时都能找到对应的初始化数据。
对于LD和GD来说__tls_get_addr的实现至关重要,下面是__tls_get_addr函数的相关定义:
rust
extern "C" fn tls_get_addr(ti: *const TlsIndex) -> *mut u8;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct TlsIndex {
pub ti_module: usize,
pub ti_offset: usize,
}
-
ti_module:有TLS段的ELF文件对应的ID,这个ID是唯一的。
-
ti_offset:要访问的变量在TLS区域中的偏移。TLS区域由.tdata和.tbss组成,每个线程有自己独有的TLS区域,偏移从0开始。
__tls_get_addr的基本流程:
-
为了支持动态加载的 TLS,每个线程维护一个 ThreadDtv 结构,它本质上是一个指针数组,索引是 ModuleID(ti_module其实就对应ModuleID)。且这个ThreadDtv是运行时创建的------当__tls_get_addr 被调用时,Relink会获得thread id,然后检测当前thread id对应的ThreadDtv是否已经创建,若未创建,则会在此时创建。
-
之后Relink会通过 GLOBAL_GENERATION 计数器检测ThreadDtv是否是最新状态,即检测是否有动态库加载或卸载,从而自动更新线程对应 DTV。
-
更新完DTV后,检查ti_module对应的 TLS 块是否分配,如果还没有分配,加载器会现场分配内存、拷贝模板数据并清零 BSS 区。
-
返回TLS变量对应的地址。即当前线程对应的TLS区域的基地址 + ti_offset。
3.2 TLS相关的重定位类型的处理
在符号链接阶段,Relink处理以下 TLS 相关的重定位,以X86-64为例:
-
R_X86_64_DTPMOD64:填入变量所在模块的 ModuleID。
-
R_X86_64_DTPOFF64:填入变量在该模块 TLS 块内部的偏移量。
-
R_X86_64_TPOFF64:处理静态 TLS 偏移,直接计算出相对于 TP 的固定位移。
-
R_X86_64_TLSDESC:现代的一种优化机制,加载器会填入一个解析函数指针和参数,允许硬件更高效地获取 TLS 地址。
LD和GD通常只会使用前两种重定位类型R_X86_64_DTPMOD64和R_X86_64_DTPOFF64,它们两填充的值分别对应前文的ti_module和ti_offset,在__tls_get_addr调用时会被使用。
IE需要配合R_X86_64_TPOFF64来使用,在处理R_X86_64_TPOFF64时,动态链接器会将TLS变量相较于TP的偏移写入GOT表中,这样配合编译器生成的代码就可以在运行时访问TLS变量了。
R_X86_64_TLSDESC比较特别,动态链接器在处理它时,会检测包含对应符号的动态库是否在程序一开始就被加载了,如果是,就可以避免__tls_get_addr这一套复杂的流程,直接使用TP加上偏移量来获得TLS变量对应的地址,若不是,最后还会走__tls_get_addr这套逻辑。
