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

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

相关推荐
wljy111 小时前
二、线程操作
linux·线程属性·线程操作
程序员爱钓鱼11 小时前
Rust match 模式匹配详解:比 if 更强大的条件分支
后端·rust
mounter62511 小时前
探索未来 AI 算力网络的基石:从传统 RoCE 走向 SRv6 驱动的弹性弹性网络(解析 Netdev 0x1A 创新实践)
linux·网络·人工智能·linux kernel·kernel·rdma·rocev2
x_x-F12 小时前
深入浅出 Linux 网络核心:sk_buff 的内存与指针流转
linux·运维·网络
潘正翔21 小时前
docker核心概念
linux·运维·服务器·docker·容器·centos·运维开发
weigangwin1 天前
agent-workspace-linux 不是远程桌面:Linux AI 工作区的隔离边界验收
linux·xvfb·mcp·桌面隔离·权限边界·bubblewrap·agent-workspace
Dovis(誓平步青云)1 天前
远程办公软件文件传输实测:6 款工具的速度、稳定性和办公体验对比
linux·运维·服务器·后端·生成对抗网络
Java小白笔记1 天前
Docker 安装配置完全指南:MacOS 、Windows、Linux环境下的安装、配置与验证
linux·macos·docker
爱吃牛肉的大老虎1 天前
rust基础之环境搭建
java·开发语言·rust
openKylin1 天前
与全球技术演进同频,openKylin 3.0从C迈向Rust
c语言·开发语言·rust·开源·开放原子·openkylin