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

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

相关推荐
bush44 小时前
嵌入式linux学习记录七,中断
linux·嵌入式
RisunJan5 小时前
Linux命令-nologin(用于系统账户或需要禁止交互式登录的场景)
linux·运维
是阿建吖!5 小时前
【Linux】信号
android·linux·c语言·c++
城北徐宫5 小时前
Linux信号深度解剖:5种产生、3张表、4次切换
linux·c++·学习
倔强的石头1065 小时前
【Linux指南】Linux快捷键与系统实用技巧
linux·运维·服务器
番茄地瓜5 小时前
Linux 配置静态 IP 步骤
linux·运维·服务器
liulilittle5 小时前
论 Linux 内核态全局稳态带宽的卡尔曼估计与工程实现
linux·服务器·网络·c++·计算机网络·tcp·通信
Irissgwe6 小时前
五、应用层协议HTTP
linux·网络·网络协议·http·状态码·url
.千余6 小时前
【Linux】 传输层协议UDP:从端口号到传输机制
linux·运维·udp
囚~徒~7 小时前
轻量化的虚拟机
linux·运维·服务器