动态链接器(十一):线程局部存储

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的基本流程:

  1. 为了支持动态加载的 TLS,每个线程维护一个 ThreadDtv 结构,它本质上是一个指针数组,索引是 ModuleID(ti_module其实就对应ModuleID)。且这个ThreadDtv是运行时创建的------当__tls_get_addr 被调用时,Relink会获得thread id,然后检测当前thread id对应的ThreadDtv是否已经创建,若未创建,则会在此时创建。

  2. 之后Relink会通过 GLOBAL_GENERATION 计数器检测ThreadDtv是否是最新状态,即检测是否有动态库加载或卸载,从而自动更新线程对应 DTV。

  3. 更新完DTV后,检查ti_module对应的 TLS 块是否分配,如果还没有分配,加载器会现场分配内存、拷贝模板数据并清零 BSS 区。

  4. 返回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这套逻辑。

4 相关代码

weizhiao/Relink: Rust no_std ELF loader and linker for dynamic loading, plugins, kernels, and embedded runtimes.https://github.com/weizhiao/Relink

相关推荐
neo_Ggx231 小时前
Linux 日志检索速查:按时间、接口、Trace ID 查询完整请求链路
java·linux·服务器
蜀道山老天师1 小时前
Prometheus监控Hadoop集群(实操完整版,含避坑指南)
大数据·linux·运维·hadoop·云原生·prometheus
biubiubiu07061 小时前
Ubuntu命令练习
linux·运维·ubuntu
曦夜日长1 小时前
Linux系统篇,开发工具(二):vim的使用与配置
linux·服务器·vim·excel
iceman19522 小时前
ubuntu 25.10升级到26.04
linux·服务器·ubuntu
拾光Ծ2 小时前
【Linux系统】线程(上)
java·linux·运维·jvm·线程·c/c++
轩轩的学习之路2 小时前
claudecode安装+第三方模型,无root
linux·人工智能·python
晓蓝WQuiet2 小时前
《鸟哥的Linux私房菜》笔记 第七至十六章
linux·运维·笔记
山岚的运维笔记2 小时前
Bash 专业人员笔记 -- 第 11 章:`true`、`false` 和 `:` 命令
linux·运维·服务器·开发语言·笔记·学习·bash