[译]All About Thread-Local Storage

线程本地存储(TLS)提供了一种为不同线程分配不同对象的机制。它是GCC扩展__thread、C11 _Thread_local和C++11 thread_local的通常实现,它们允许使用声明的名称来指代与当前线程相关的实体。本文将详细描述ELF平台上的线程本地存储,并触及其他相关话题,如:线程特定数据键和Windows/MacOS TLS。

线程本地存储的一个例子是POSIX errno

Each thread has its own thread ID, scheduling priority and policy, errno value, floating point environment, thread-specific key/value bindings, and the required system resources to support a flow of control.

每个线程都有自己的线程ID、调度优先级和策略、errno值、浮点环境、线程特定的键/值绑定,以及支持控制流的所需系统资源。

不同的线程有不同的errno副本。errno通常被定义为一个函数,它返回一个线程本地的变量。

对于每个架构,权威的ELF ABI文件是System V ABI(通用ABI)的处理器补充(psABI)。这些文件通常参考Ulrich Drepper的The ELF Handling for Thread-Local Storage。然而,该文件混合了一般的规范和glibc内部的内容。

Representation

汇编程序的行为

编译器通常在.tdata.tbss部分定义线程本地变量(这些部分的标志是SHF_TLS)。代表线程本地变量的符号具有STT_TLS类型(代表线程本地存储实体)。在GNU as语法中,你可以用.type a@tls_object给一个类型的STT_TLS。一个TLS符号的st_value值是相对于定义部分的偏移。

1
2
3
4
5
6
7
8
9
10
.section .tbss,"awT",@nobits
.globl a, b
.type a, @tls_object
.type b, @tls_object
a:
.zero 4
.size a, .-a
b:
.zero 4
.size b, .-b

在这个例子中,st_value(a)=0同时st_value(b)=4

在Clang和GCC产生的汇编中,线程局部变量被注释为.type a@object(STT_OBJECT)。当汇编器看到这种符号被定义在SHF_TLS部分或被TLS重定位所引用时,STT_NOTYPE/STT_OBJECT将被升级为STT_TLS

GNU支持一个指令.tls_common,它定义了STT_TLSSHN_COMMON符号。这是一个不明显的特征。目前还不清楚GCC是否仍有一个发出.tls_common指令的代码路径。LLVM集成汇编器不支持.tls_common

链接器行为

链接器将.tdata输入部分合并为.tdata输出部分。.tbss的输入部分被组合成一个.tbss的输出部分。两个SHF_TLS的输出部分被放置在PT_TLS的程序头中。

  • p_offset: TLS初始化镜像的文件偏移量

  • p_vaddr: TLS初始化映像的虚拟地址

  • p_filesz: TLS初始化映像的大小

  • p_memsz: 线程本地存储的总大小。最后的p_memsz-p_filesz字节将被动态加载器清零。

  • p_align:对齐方式

PT_TLS程序头包含在PT_LOAD程序头中。如果使用PT_GNU_RELROPT_TLS被包含在一个PT_GNU_RELRO中,而PT_GNU_RELRO被包含在一个PT_LOAD中。概念上PT_TLSSTT_TLS符号就像在一个独立的地址空间。动态加载器应该把TLS初始化 image 的[p_vaddr,p_vaddr+p_filesz)复制到相应的静态TLS块中。

在可执行文件和共享对象文件中,st_value通常持有一个虚拟地址。对于一个STT_TLS符号,st_value持有相对于PT_TLS程序头的虚拟地址的偏移。PT_TLS的第一个字节是由st_value==0的TLS符号引用的。

GNU ld 将 STT_TLS SHN_COMMON 符号视为定义在 .tcommon 部分。它的内部链接器脚本将这些部分放到输出部分.tdata中。LLD 不支持 STT_TLS SHN_COMMON 符号。

动态加载器行为

动态加载器从主可执行文件和立即加载的共享对象中收集PT_TLS程序头文件(通过过渡的DT_NEEDED),并分配静态TLS块,每个PT_TLS一个块。对于每个PT_TLS,动态加载器从TLS初始化镜像中复制p_filesz字节到TLS块中,并将尾部的p_memsz-p_filesz字节设置为零。

对于主可执行文件的静态TLS块,模块ID是1,TLS符号的TP偏移量是一个链接时间常数。链接器和动态加载器共享相同的公式。

对于在程序开始时加载的共享对象,从线程指针到其静态TLS块的偏移量在程序开始时是一个固定值,尽管不是一个链接时常数。该偏移量可以被初始执行的TLS模型所使用的GOT动态重定位所引用。

ELF对线程本地存储的处理描述了两种TLS的变体,并指定了它们的数据结构。然而,只有主可执行文件的静态TLS块的TP偏移是一个硬性要求。尽管如此,libc的实现通常将静态TLS块放在一起,并为线程控制块和静态TLS块分配了一个空间。

对于一个由pthread_create创建的新线程,静态TLS块通常被分配为线程堆栈的一部分。如果在堆栈的最大地址和线程控制块之间没有一个保护页,这可能被认为是脆弱的,因为堆栈溢出可以覆盖线程控制块。

模型

本地执行 TLS 模型(执行与非抢占)

这是最有效的TLS模型。它适用于在可执行文件中定义TLS符号的情况。

编译器在-fno-pic/fpie模式下选择这种模式,如果变量是

  • 一个定义

  • 或具有非默认可见性的声明。

第一个条件是显而易见的。第二个条件是由于非默认可见性意味着该变量必须由可执行文件中的另一个翻译单元定义。

1
2
3
_Thread_local int def;
__attribute__((visibility("hidden"))) extern thread_local int ref;
int foo() { return def + ref; }
1
2
# x86-64
movl %fs:def@TPOFF, %eax

原文

原文为:All about thread-local storage

[译]All About Thread-Local Storage

https://hoooo.org/2021/10/11/tls/

作者

Hu

发布于

2021-10-11

更新于

2021-10-11

许可协议

评论