编译环境支持声明线程局部数据。此类数据有时称为线程特定数据或线程专用数据,但更多时候用首字母缩略词 TLS 表示。通过将变量声明为线程局部变量,编译器会自动安排针对每个线程分配这些变量。
提供对此功能的内置支持有三个目的。
提供生成 POSIX 接口的基础,此接口用于分配线程特定数据。
提供一种方便高效的机制,以便应用程序和库直接使用线程局部变量。
执行循环并行优化时,编译器可以根据需要分配 TLS。
使用 __thread 关键字可将变量声明为线程局部变量,如下例所示。
__thread int i; __thread char *p; __thread struct state s;
在循环优化期间,编译器可根据需要选择创建临时线程局部变量。
__thread 关键字可以应用于任何全局变量、文件范围的静态变量或函数范围的静态变量。它对始终为线程局部变量的自动变量没有影响。
在 C++ 中,如果初始化需要静态构造函数,则无法初始化线程局部变量。否则,可以将线程局部变量初始化为对普通静态变量合法的任何值。
无论是线程局部变量还是其他变量都不能静态初始化为线程局部变量的地址。
可以在外部声明和引用线程局部变量。线程局部变量遵循与普通符号相同的插入规则。
可以在进程启动期间或进程启动之后通过延迟装入、过滤器或 dlopen(3C) 动态装入共享库。如果使用动态 TLS 模型编译每个包含对线程局部变量的引用的转换单元,则可以在启动后装入包含该引用的共享库。
静态 TLS 模型生成执行速度更快的代码。但是,编译为使用此模型的代码不能在启动后动态装入的库中引用线程局部变量。动态 TLS 模型可以引用所有 TLS。这些模型将在线程局部存储的访问模型中介绍。
寻址运算符 & 可用于线程局部变量。此运算符在运行时计算,并返回当前线程中变量的地址。进程中的任何线程都可自由使用由此运算符获取的地址,只要计算该地址的线程仍然存在。当线程终止时,指向该线程中的线程局部变量的所有指针将变为无效。
使用 dlsym(3C) 获取线程局部变量的地址时,返回的地址是调用 dlsym() 的线程中该变量的实例地址。
编译时已分配的线程局部数据的独立副本必须与各个执行线程关联。要提供此数据,可使用 TLS 节指定大小和初始内容。
编译环境在使用 SHF_TLS 标志标识的节中分配 TLS。这些节根据存储的声明方式提供已初始化的 TLS 和未初始化的 TLS。
已初始化的线程局部变量在 .tdata 或 .tdata1 节中分配。此初始化可能需要重定位。
未初始化的线程局部变量定义为 COMMON 符号。最终分配在 .tbss 节中进行。
在分配了任何已初始化的节后会立即分配未初始化的节,并进行填充以便正确对齐。合并的节一起构成 TLS 模板,每次创建新线程时,可使用此模板分配 TLS。
此模板的已初始化部分称为 TLS 初始化映像。将由于已初始化的线程局部变量而产生的所有重定位应用于此模板。当新线程需要初始值时,将使用重定位的值。
TLS 符号的符号类型为 STT_TLS。这些符号被指定相对于 TLS 模板的开头的偏移。与这些符号关联的实际虚拟地址无关紧要。地址仅指向模板,不指向每个数据项的每线程副本。
在动态可执行文件和共享库中,对于已定义的符号,STT_TLS 符号的 st_value 字段包含指定的偏移,或者对于未定义的符号,此字段包含零。
定义了多个重定位以支持访问 TLS。请参阅SPARC: 线程局部存储的重定位类型、32 位 x86: 线程局部存储的重定位类型和x64: 线程局部存储的重定位类型。TLS 重定位仅引用 STT_TLS 类型的符号。
在动态可执行文件和共享库中,PT_TLS 程序项描述 TLS 模板。此模板包含以下成员。
表 8–1 ELF PT_TLS 程序头项
成员 |
值 |
---|---|
p_offset |
TLS 初始化映像的文件偏移 |
p_vaddr |
TLS 初始化映像的虚拟内存地址 |
p_paddr |
0 |
p_filesz |
TLS 初始化映像的大小 |
p_memsz |
TLS 模板的总大小 |
p_flags |
PF_R |
p_align |
TLS 模板的对齐方式 |
在程序的生命周期中,将会在三个时间创建 TLS。
程序启动时。
创建新线程时。
程序启动后装入共享库之后,线程第一次引用 TLS 块时。
运行时线程局部数据存储的布局如图 8–1 中所示。
在程序启动时,运行时系统将为主线程创建 TLS。
首先,运行时链接程序以逻辑方式将所有已装入动态库(包括动态可执行文件)的 TLS 模板合并成单个静态模板。在合并的模板中为每个动态库的 TLS 模板指定一个偏移 (tlsoffsetm),如下所示。
tlsoffset1 = round(tlssize1, align1 )
tlsoffsetm+1 = round(tlsoffsetm + tlssizem+1, alignm+1)
tlssizem+1 和 alignm+1 分别是动态库 m 的分配模板的大小和对齐方式。其中,1 <= m <= M,M 是已装入动态库的总数。round(offset, align) 函数返回向上舍入为最接近 align 倍数的偏移。TLS 模板正好放置在线程指针 tpt 的前面。根据从 tpt 中减去的数字访问 TLS 数据。
接下来,运行时链接程序将计算启动时为 TLS 分配的总大小 tlssizeS。此大小等于 tlsoffsetM。
然后,运行时链接程序构造初始化记录的链接表。此列表中的每条记录都描述一个已装入动态库的 TLS 初始化映像。每条记录都包含以下字段。
指向 TLS 初始化映像的指针。
TLS 初始化映像的大小。
目标文件的 tlsoffsetm。
指示目标文件是否使用静态 TLS 模型的标志。
线程库使用此信息为初始线程分配存储空间。将初始化此存储空间,并为初始线程创建动态 TLS 向量。
对于初始线程和所创建的每个新线程,线程库将为每个已装入动态库分配一个新的 TLS 块。各个块可以单独进行分配,也可以作为一个连续块进行分配。
每个线程 t 都有一个关联的线程指针 tpt,该指针指向线程控制块 TCB。线程指针 tp 始终包含当前正在运行的线程的 tpt 值。
然后,线程库为当前线程 t 创建一个指针向量 dtvt。每个向量的第一个元素都包含一个生成编号 gent,该生成编号用于确定需要扩展向量的时间。 请参见延迟分配线程局部存储块。
向量 dtvt,m 中剩余的每个元素都是一个指针(指向为属于动态库 m 的 TLS 保留的块)。
对于启动后动态装入的目标文件,线程库延迟分配 TLS 块。将在第一次引用已装入目标文件中的 TLS 变量时进行分配。必须使用动态 TLS 模型引用在启动后动态装入的目标文件中定义的 TLS。对于延迟分配的块,将指针 dtvt,m 设置为实现定义的特殊值。
运行时链接程序可以将所有启动目标文件的 TLS 模板进行分组,以便在向量 dtv t,1 中共享单个元素。这种分组不会影响前面介绍的偏移计算,也不会影响初始化记录列表的创建。但是,对于以下各节,总目标文件数 M 的值从值 1 开始。
然后,线程库将初始化映像复制到新存储块中的相应位置。
可以在进程启动后装入包含 TLS 的共享库。因此,运行时链接程序必须扩展初始化记录列表,以包含新目标文件的初始化模板。为新目标文件指定索引 m = M + 1,并以 1 为增量累加计数器 M。但是,直到实际引用块时才会分配新的 TLS 块。
卸载包含 TLS 的库时,将释放该库使用的 TLS 块。
在动态 TLS 模型中,当线程 t 需要访问目标文件 m 的 TLS 块时,代码将更新 dtvt 并执行 TLS 块的初始分配。线程库将提供以下接口以便动态分配 TLS。
typedef struct { unsigned long ti_moduleid; unsigned long ti_tlsoffset; } TLS_index; extern void * __tls_get_addr(TLS_index * ti); (SPARC and x64) extern void * ___tls_get_addr(TLS_index * ti); (32–bit x86)
此函数的 SPARC 和 64 位 x86 定义具有相同的函数签名。但是,32 位 x86 版本不使用缺省调用约定来传递栈中的参数。相反,32 位 x86 版本通过更有效的 %eax 寄存器来传递其参数。为了表示将使用此替代调用方法,32 位 x86 函数的名称中有三个前导下划线。
这两个版本的 tls_get_addr() 都检查每线程生成计数器 gent,以便确定是否需要更新向量。如果向量 dtvt 已过时,则例程将更新此向量,可能会重新分配此向量以便为更多项留出空间。然后,例程将检查是否已分配与 dtvt,m 对应的 TLS 块。如果尚未分配此向量,则例程将使用运行时链接程序提供的初始化记录列表中的信息来分配并初始化块。指针 dtv t,m 被设置为指向已分配的块。例程返回一个指向块中的给定偏移的指针。
每个 TLS 引用都遵循下列其中一个访问模型。这些模型按照最常见、但最少优化到速度最快、但限制最大的顺序列出。
此模型允许从共享库或动态可执行文件引用所有 TLS 变量。如果第一次从特定线程引用 TLS 块,则此模型还支持延迟分配此块。
此模型是 GD 模型的优化模型。如果编译器确定变量在要生成的动态库中将局部绑定或受到保护,则编译器将指示链接编辑器静态绑定动态 tlsoffset 并使用此模型。与 GD 模型相比,此模型可提供更好的性能。每个函数只需要调用一次 tls_get_addr(),即可确定 dtv0,m 地址。将在链接编辑时绑定的动态 TLS 偏移会与每个引用的 dtv0,m 地址相加。
此模型只能引用初始静态 TLS 模板中包含的 TLS 变量。此模板由进程启动时可用的所有 TLS 块组成。在此模型中,给定变量 x 的相对于线程指针的偏移存储在 x 的 GOT 项中。此模型不能从在初始进程启动后通过延迟装入、过滤器或 dlopen(3C) 装入的共享库引用 TLS 变量。此模型不能访问使用延迟分配的 TLS 块。
此模型只能引用动态可执行文件的 TLS 块中包含的 TLS 变量。链接编辑器静态计算相对于线程指针的偏移,而不需要进行动态重定位或额外引用 GOT。不能使用此模型引用动态可执行文件外部的变量。
链接编辑器可以将代码从更常规的访问模型转换为更优化的模型(如果确定适合进行转换)。使用独特的 TLS 重定位可实现此转换。这些重定位不仅请求执行更新,还标识要使用的 TLS 访问模型。
链接编辑器在了解 TLS 访问模型和要创建的目标文件类型后可以执行转换。例如,如果要将使用 GD 访问模型的可重定位目标文件链接到动态可执行文件,则链接编辑器可以根据需要转换使用 IE 或 LE 访问模型的引用。然后执行模型所需的重定位。
下图说明了不同的访问模型,以及一个模型可以转换为另一个模型的时间。
在 SPARC 上,可使用以下代码序列模型来访问线程局部变量。
此代码序列实现线程局部存储的访问模型中介绍的 GD 模型。
表 8–2 SPARC: 常规动态线程局部变量的访问代码
sethi 和 add 指令分别生成 R_SPARC_TLS_GD_HI22 和 R_SPARC_TLS_GD_LO10 重定位。这些重定位指示链接编辑器在 GOT 中分配空间,以存储变量 x 的 TLS_index 结构。链接编辑器通过将相对于 GOT 的偏移替换为新的 GOT 项来处理此重定位。
x 的装入目标文件索引和 TLS 块索引在运行前无法确定。因此,链接编辑器根据 GOT 放置 R_SPARC_TLS_DTPMOD32 和 R_SPARC_TLS_DPTOFF32 重定位,以便运行时链接程序处理。
第二个 add 指令导致生成 R_SPARC_TLS_GD_ADD 重定位。仅当链接编辑器将 GD 代码序列更改为另一个序列时,才使用此重定位。
call 指令使用特殊语法 x@TLSPLT。此调用引用 TLS 变量并生成 R_SPARC_TLS_GD_CALL 重定位。此重定位指示链接编辑器将调用绑定到 __tls_get_addr() 函数,并将 call 指令与 GD 代码序列关联。
add 指令必须出现在 call 指令前面。不能将 add 指令放在调用的延迟槽中。由于后面进行的代码变换需要已知顺序,所以必须满足此要求。
用作 add 指令(由 R_SPARC_TLS_GD_ADD 重定位标记)的 GOT 指针的寄存器必须是 add 指令中的第一个寄存器。在代码变换期间,此要求允许链接编辑器标识 GOT 指针寄存器。
此代码序列实现线程局部存储的访问模型中介绍的 LD 模型。
表 8–3 SPARC: 局部动态线程局部变量的访问代码
第一个 sethi 指令和 add 指令分别生成 R_SPARC_TLS_LDM_HI22 和 R_SPARC_TLS_LDM_LO10 重定位。这些重定位指示链接编辑器在 GOT 中分配空间,以存储当前目标文件的 TLS_index 结构。链接编辑器通过将相对于 GOT 的偏移替换为新的 GOT 项来处理此重定位。
装入目标文件索引在运行前无法确定。因此,将创建 R_SPARC_TLS_DTPMOD32 重定位,并在 TLS_index 结构的 ti_tlsoffset 字段中填充零。
第二个 add 和 call 指令分别由 R_SPARC_TLS_LDM_ADD 和 R_SPARC_TLS_LDM_CALL 重定位标记。
后面的 sethi 指令和 xor 指令分别生成 R_SPARC_LDO_HIX22 和 R_SPARC_TLS_LDO_LOX10 重定位。在链接编辑时将确定每个局部符号的 TLS 偏移,因此将直接填充这些值。add 指令使用 R_SPARC_TLS_LDO_ADD 重定位标记。
当一个过程引用多个局部符号时,编译器将生成一次获取 TLS 块的基本地址的代码。然后,使用此基本地址计算每个符号的地址,而不需要单独调用库。
包含 add 指令(由 R_SPARC_TLS_LDO_ADD 标记)中的 TLS 目标文件地址的寄存器必须是指令序列中的第一个寄存器。在代码变换期间,此要求允许链接编辑器标识寄存器。
此代码序列实现线程局部存储的访问模型中介绍的 IE 模型。
表 8–4 32 位 SPARC: 初始可执行的线程局部变量的访问代码
sethi 指令和 or 指令分别生成 R_SPARC_TLS_IE_HI22 和 R_SPARC_TLS_IE_LO10 重定位。这些重定位指示链接编辑器在 GOT 中创建空间,以存储符号 x 的静态 TLS 偏移。针对 GOT 的 R_SPARC_TLS_TPOFF32 重定位未完成,以便运行时链接程序使用符号 x 的负静态 TLS 偏移填充。ld 和 add 指令分别使用 R_SPARC_TLS_IE_LD 和 R_SPARC_TLS_IE_ADD 重定位标记。
用作 add 指令(由 R_SPARC_TLS_IE_ADD 重定位标记)的 GOT 指针的寄存器必须是此指令中的第一个寄存器。在代码变换期间,此要求允许链接编辑器标识 GOT 指针寄存器。
此代码序列实现线程局部存储的访问模型中介绍的 IE 模型。
表 8–5 64 位 SPARC: 初始可执行的线程局部变量的访问代码
此代码序列实现线程局部存储的访问模型中介绍的 LE 模型。
表 8–6 SPARC: 局部可执行的线程局部变量的访问代码
sethi 和 xor 指令分别生成 R_SPARC_TLS_LE_HIX22 和 R_SPARC_TLS_LE_LOX10 重定位。链接编辑器将这些重定位直接绑定到在可执行文件中定义的符号的静态 TLS 偏移。在运行时不需要进行重定位处理。
下表中列出的 TLS 重定位是针对 SPARC 定义的。此表中的说明使用以下表示法。
在 GOT 中分配两个连续项,以存储 TLS_index 结构。此信息将传递给 __tls_get_addr()。引用此项的指令将绑定到两个 GOT 项中的第一项的地址。
在 GOT 中分配两个连续项,以存储 TLS_index 结构。此信息将传递给 __tls_get_addr()。将此结构的 ti_tlsoffset 字段设置为 0,并且在运行时填充 ti_moduleid。对 __tls_get_addr() 的调用将返回动态 TLS 块的起始偏移。
计算相对于 TLS 块的 tlsoffset。
计算相对于静态 TLS 块的负 tlsoffset。将此值与线程指针相加以计算 TLS 地址。
计算包含 TLS 符号的目标文件的标识符。
名称 |
值 |
字段 |
计算 |
---|---|---|---|
R_SPARC_TLS_GD_HI22 |
56 |
T-simm22 |
@dtlndx(S + A) >> 10 |
R_SPARC_TLS_GD_LO10 |
57 |
T-simm13 |
@dtlndx(S + A) & 0x3ff |
R_SPARC_TLS_GD_ADD |
58 |
无 |
请参阅此表后面的说明。 |
R_SPARC_TLS_GD_CALL |
59 |
V-disp30 |
请参阅此表后面的说明。 |
R_SPARC_TLS_LDM_HI22 |
60 |
T-simm22 |
@tmndx(S + A) >> 10 |
R_SPARC_TLS_LDM_LO10 |
61 |
T-simm13 |
@tmndx(S + A) & 0x3ff |
R_SPARC_TLS_LDM_ADD |
62 |
无 |
请参阅此表后面的说明。 |
R_SPARC_TLS_LDM_CALL |
63 |
V-disp30 |
请参阅此表后面的说明。 |
R_SPARC_TLS_LDO_HIX22 |
64 |
T-simm22 |
@dtpoff(S + A) >> 10 |
R_SPARC_TLS_LDO_LOX10 |
65 |
T-simm13 |
@dtpoff(S + A) & 0x3ff |
R_SPARC_TLS_LDO_ADD |
66 |
无 |
请参阅此表后面的说明。 |
R_SPARC_TLS_IE_HI22 |
67 |
T-simm22 |
@got(@tpoff(S + A)) >> 10 |
R_SPARC_TLS_IE_LO10 |
68 |
T-simm13 |
@got(@tpoff(S + A)) & 0x3ff |
R_SPARC_TLS_IE_LD |
69 |
无 |
请参阅此表后面的说明。 |
R_SPARC_TLS_IE_LDX |
70 |
无 |
请参阅此表后面的说明。 |
R_SPARC_TLS_IE_ADD |
71 |
无 |
请参阅此表后面的说明。 |
R_SPARC_TLS_LE_HIX22 |
72 |
T-imm22 |
(@tpoff(S + A) ^0xffffffffffffffff) >> 10 |
R_SPARC_TLS_LE_LOX10 |
73 |
T-simm13 |
(@tpoff(S + A) & 0x3ff) | 0x1c00 |
R_SPARC_TLS_DTPMOD32 |
74 |
V-word32 |
@dtpmod(S + A) |
R_SPARC_TLS_DTPMOD64 |
75 |
V-word64 |
@dtpmod(S + A) |
R_SPARC_TLS_DTPOFF32 |
76 |
V-word32 |
@dtpoff(S + A) |
R_SPARC_TLS_DTPOFF64 |
77 |
V-word64 |
@dtpoff(S + A) |
R_SPARC_TLS_TPOFF32 |
78 |
V-word32 |
@tpoff(S + A) |
R_SPARC_TLS_TPOFF64 |
79 |
V-word64 |
@tpoff(S + A) |
一些重定位类型的语义不只是简单的计算。
此重定位标记 GD 代码序列的 add 指令。用于 GOT 指针的寄存器是此序列中的第一个寄存器。此重定位标记的指令出现在 R_SPARC_TLS_GD_CALL 重定位标记的 call 指令前面。此重定位用于在链接编辑时在 TLS 模型之间进行转换。
可以按处理引用 __tls_get_addr() 函数的 R_SPARC_WPLT30 重定位的方式处理此重定位。此重定位是 GD 代码序列的一部分。
此重定位标记 LD 代码序列的第一个 add 指令。用于 GOT 指针的寄存器是此序列中的第一个寄存器。此重定位标记的指令出现在 R_SPARC_TLS_GD_CALL 重定位标记的 call 指令前面。此重定位用于在链接编辑时在 TLS 模型之间进行转换。
可以按处理引用 __tls_get_addr() 函数的 R_SPARC_WPLT30 重定位的方式处理此重定位。此重定位是 LD 代码序列的一部分。
此重定位标记 LD 代码序列中的最后一个 add 指令。包含目标文件地址(在代码序列的初始部分中计算得出)的寄存器是此指令中的第一个寄存器。此重定位允许链接编辑器标识此寄存器以进行代码变换。
此重定位标记 32 位 IE 代码序列中的 ld 指令。此重定位用于在链接编辑时在 TLS 模型之间进行转换。
此重定位标记 64 位 IE 代码序列中的 ldx 指令。此重定位用于在链接编辑时在 TLS 模型之间进行转换。
此重定位标记 IE 代码序列中的 add 指令。用于 GOT 指针的寄存器是此序列中的第一个寄存器。
在 x86 上,可使用以下代码序列模型来访问 TLS。
此代码序列实现线程局部存储的访问模型中介绍的 GD 模型。
表 8–8 32 位 x86: 常规动态线程局部变量的访问代码
leal 指令生成 R_386_TLS_GD 重定位,此重定位指示链接编辑器在 GOT 中分配空间,以存储变量 x 的 TLS_index 结构。链接编辑器通过将相对于 GOT 的偏移替换为新的 GOT 项来处理此重定位。
由于在运行前无法确定 x 的装入目标文件索引和 TLS 块索引,因此链接编辑器将根据 GOT 放置 R_386_TLS_DTPMOD32 和 R_386_TLS_DTPOFF32 重定位,以便运行时链接程序处理。生成的 GOT 项的地址将装入寄存器 %eax 中以便调用 ___tls_get_addr()。
call 指令导致生成 R_386_TLS_GD_PLT 重定位。此重定位指示链接编辑器将调用绑定到 ___tls_get_addr() 函数,并将 call 指令与 GD 代码序列关联。
call 指令必须紧跟在 leal 指令后面。要允许进行代码变换,必须满足此要求。
此代码序列实现线程局部存储的访问模型中介绍的 LD 模型。
表 8–9 32 位 x86: 局部动态线程局部变量的访问代码
第一个 leal 指令生成 R_386_TLS_LDM 重定位。此重定位指示链接编辑器在 GOT 中分配空间,以存储当前目标文件的 TLS_index 结构。链接编辑器通过将相对于 GOT 的偏移替换为新的链接表项来处理此重定位。
装入目标文件索引在运行前无法确定。因此,将创建 R_386_TLS_DTPMOD32 重定位,并在此结构的 ti_tlsoffset 字段中填充零。call 指令使用 R_386_TLS_LDM_PLT 重定位标记。
在链接编辑时将确定每个局部符号的 TLS 偏移,因此链接编辑器将直接填充这些值。
当一个过程引用多个局部符号时,编译器将生成一次获取 TLS 块的基本地址的代码。然后,使用此基本地址计算每个符号的地址,而不需要单独调用库。
此代码序列实现线程局部存储的访问模型中介绍的 IE 模型。
IE 模型存在两个代码序列。一个序列用于使用 GOT 指针的位置无关代码。另一个序列用于不使用 GOT 指针的位置相关代码。
表 8–10 32 位 x86: 初始可执行的、位置无关线程局部变量的访问代码
代码序列 |
初始重定位 |
符号 |
0x00 movl %gs:0, %eax 0x06 addl x@gotntpoff(%ebx), %eax # %eax - contains address of TLS variable |
<none> R_386_TLS_GOTIE |
x |
未完成的重定位 |
符号 |
|
GOT[n] |
R_386_TLS_TPOFF |
x |
addl 指令生成 R_386_TLS_GOTIE 重定位。此重定位指示链接编辑器在 GOT 中创建空间,以存储符号 x 的静态 TLS 偏移。针对 GOT 表的 R_386_TLS_TPOFF 重定位未完成,以便运行时链接程序使用符号 x 的静态 TLS 偏移填充。
表 8–11 32 位 x86: 初始可执行的、位置相关线程局部变量的访问代码
代码序列 |
初始重定位 |
符号 |
0x00 movl %gs:0, %eax 0x06 addl x@indntpoff, %eax # %eax - contains address of TLS variable |
<none> R_386_TLS_IE |
x |
未完成的重定位 |
符号 |
|
GOT[n] |
R_386_TLS_TPOFF |
x |
addl 指令生成 R_386_TLS_IE 重定位。此重定位指示链接编辑器在 GOT 中创建空间,以存储符号 x 的静态 TLS 偏移。此序列和位置无关序列之间的主要差别在于,指令直接绑定到所创建的 GOT 项,而不是使用 GOT 指针寄存器的偏移。针对 GOT 的 R_386_TLS_TPOFF 重定位未完成,以便运行时链接程序使用符号 x 的静态 TLS 偏移填充。
将偏移直接嵌入到内存引用中可以装入变量 x 的内容(而不是地址),如下面两个序列中所示。
表 8–12 32 位 x86: 初始可执行的、位置无关动态线程局部变量的访问代码
代码序列 |
初始重定位 |
符号 |
0x00 movl x@gotntpoff(%ebx), %eax 0x06 movl %gs:(%eax), %eax # %eax - contains address of TLS variable |
R_386_TLS_GOTIE <none> |
x |
未完成的重定位 |
符号 |
|
GOT[n] |
R_386_TLS_TPOFF |
x |
表 8–13 32 位 x86: 初始可执行的、位置无关线程局部变量的访问代码
代码序列 |
初始重定位 |
符号 |
0x00 movl x@indntpoff, %ecx 0x06 movl %gs:(%ecx), %eax # %eax - contains address of TLS variable |
R_386_TLS_IE <none> |
x |
未完成的重定位 |
符号 |
|
GOT[n] |
R_386_TLS_TPOFF |
x |
在最后一个序列中,如果使用的是 %eax 寄存器而不是 %ecx 寄存器,则第一个指令的长度可为 5 或 6 字节。
此代码序列实现线程局部存储的访问模型中介绍的 LE 模型。
表 8–14 32 位 x86: 局部可执行的线程局部变量的访问代码
代码序列 |
初始重定位 |
符号 |
0x00 movl %gs:0, %eax 0x06 leal x@ntpoff(%eax), %eax # %eax - contains address of TLS variable |
<none> R_386_TLS_LE |
x |
movl 指令生成 R_386_TLS_LE_32 重定位。链接编辑器将此重定位直接绑定到在可执行文件中定义的符号的静态 TLS 偏移。在运行时不需要进行任何处理。
通过使用以下指令序列,借助相同重定位可以访问变量 x 的内容而不是地址。
表 8–15 32 位 x86: 局部可执行的线程局部变量的访问代码
代码序列 |
初始重定位 |
符号 |
0x00 movl %gs:0, %eax 0x06 movl x@ntpoff(%eax), %eax # %eax - contains address of TLS variable |
<none> R_386_TLS_LE |
x |
可以使用以下序列实现从变量装入或存储到变量,而不必计算变量的地址。请注意,x@ntpoff 表达式不能用作立即值,而应用作绝对地址。
表 8–16 32 位 x86: 局部可执行的线程局部变量的访问代码
代码序列 |
初始重定位 |
符号 |
0x00 movl %gs:x@ntpoff, %eax # %eax - contains address of TLS variable |
R_386_TLS_LE |
x |
下表中列出的 TLS 重定位是针对 x86 定义的。此表中的说明使用以下表示法。
在 GOT 中分配两个连续项,以存储 TLS_index 结构。此结构将传递给 ___tls_get_addr()。引用此项的指令将绑定到两个 GOT 项中的第一项。
可以按处理引用 ___tls_get_addr() 函数的 R_386_PLT32 重定位的方式处理此重定位。
在 GOT 中分配两个连续项,以存储 TLS_index 结构。此结构将传递给 ___tls_get_addr()。将 TLS_index 的 ti_tlsoffset 字段设置为 0,并且在运行时填充 ti_moduleid。对 ___tls_get_addr() 的调用将返回动态 TLS 块的起始偏移。
在 GOT 中分配一项,并使用相对于静态 TLS 块的负 tlsoffset 初始化该项。运行时将使用 R_386_TLS_TPOFF 重定位执行此序列。
此表达式类似于 @gotntpoff,但它用在位置相关代码中。在 movl 或 addl 指令中,@gotntpoff 将解析为相对于 GOT 起始位置的 GOT 插槽地址。@indntpoff 将解析为绝对 GOT 插槽地址。
计算相对于静态 TLS 块的负 tlsoffset。
计算相对于 TLS 块的 tlsoffset。此值用作加数的立即值,并且不与特定寄存器关联。
计算包含 TLS 符号的目标文件的标识符。
名称 |
值 |
字段 |
计算 |
---|---|---|---|
R_386_TLS_GD_PLT |
12 |
Word32 |
@tlsgdplt |
R_386_TLS_LDM_PLT |
13 |
Word32 |
@tlsldmplt |
R_386_TLS_TPOFF |
14 |
Word32 |
@ntpoff(S) |
R_386_TLS_IE |
15 |
Word32 |
@indntpoff(S) |
R_386_TLS_GOTIE |
16 |
Word32 |
@gotntpoff(S) |
R_386_TLS_LE |
17 |
Word32 |
@ntpoff(S) |
R_386_TLS_GD |
18 |
Word32 |
@tlsgd(S) |
R_386_TLS_LDM |
19 |
Word32 |
@tlsldm(S) |
R_386_TLS_LDO_32 |
32 |
Word32 |
@dtpoff(S) |
R_386_TLS_DTPMOD32 |
35 |
Word32 |
@dtpmod(S) |
R_386_TLS_DTPOFF32 |
36 |
Word32 |
@dtpoff(S) |
在 x64 上,可使用以下代码序列模型来访问 TLS。
此代码序列实现线程局部存储的访问模型中介绍的 GD 模型。
表 8–18 x64: 常规动态线程局部变量的访问代码
__tls_get_addr() 函数采用单个参数,即 tls_index 结构的地址。与 x@tlsgd(%rip) 表达式关联的 R_AMD64_TLSGD 重定位指示链接编辑器在 GOT 中分配 tls_index 结构。tls_index 结构所需的两个元素将保留在连续的 GOT 项(GOT[n] 和 GOT[n+1])中。这些 GOT 项与 R_AMD64_DTPMOD64 和 R_AMD64_DTPOFF64 重定位关联。
地址 0x00 处的指令计算第一个 GOT 项的地址。此计算将 GOT 起始位置的 PC 相对地址(在链接编辑时确定)与当前指令指针相加。使用 %rdi 寄存器将结果传递给 __tls_get_addr() 函数。
leaq 指令计算第一个 GOT 项的地址。将 GOT 的 PC 相对地址(在链接编辑时确定)与当前指令指针相加来执行此计算。.byte、.word 和 .rex64 前缀确保整个指令序列占用 16 字节。由于这些前缀不会对代码造成负面影响,因此可以使用这些前缀。
此代码序列实现线程局部存储的访问模型中介绍的 LD 模型。
表 8–19 x64: 局部动态线程局部变量的访问代码
前两个指令与用于常规动态模型的代码序列等效,虽然不进行任何填充。这两个指令必须是连续的。x1@tlsld(%rip) 序列为符号 x1 生成 tls_index 项。此索引是指包含偏移为零的 x1 的当前模块。链接编辑器为目标文件 R_AMD64_DTMOD64 创建一个重定位。
因为各个偏移会单独装入,所以不需要 R_AMD64_DTOFF32 重定位。x1@dtpoff 表达式用于访问符号 x1 的偏移。将指令用作地址 0x10 时,可装入完整的偏移并将该偏移与 %rax 中的 __tls_get_addr() 调用的结果相加,以在 %rcx 中生成结果。x1@dtpoff 表达式创建 R_AMD64_DTPOFF32 重定位。
可以使用以下指令装入变量的值,而不必计算变量的地址。此指令与原始 leaq 指令创建相同的重定位。
movq x1@dtpoff(%rax), %r11 |
如果 TLS 块的基本地址保存在一个寄存器中,则装入、存储或计算受保护的线程局部变量的地址需要一个指令。
使用局部动态模型比使用常规动态模型可获得更多好处。访问其他每个线程局部变量只需要三个新指令。此外,不需要其他 GOT 项或运行时重定位。
此代码序列实现线程局部存储的访问模型中介绍的 IE 模型。
表 8–20 x64: 初始可执行的线程局部变量的访问代码
代码序列 |
初始重定位 |
符号 |
0x00 movq %fs:0, %rax 0x09 addq x@gottpoff(%rip), %rax # %rax - contains address of TLS variable |
<none> R_AMD64_GOTTPOFF |
x |
未完成的重定位 |
符号 |
|
GOT[n] |
R_AMD64_TPOFF64 |
x |
符号 x 的 R_AMD64_GOTTPOFF 重定位请求链接编辑器生成 GOT 项和关联的 R_AMD64_TPOFF64 重定位。然后,x@gottpoff(%rip) 指令使用 GOT 项相对于此指令结束位置的偏移。R_AMD64_TPOFF64 重定位使用根据目前所装入模块确定的符号 x 的值。偏移将写入到 GOT 项中,然后由 addq 指令装入。
要装入 x 的内容(而不是 x 的地址),可使用以下序列。
表 8–21 x64: 初始可执行的线程局部变量的访问代码 II
代码序列 |
初始重定位 |
符号 |
0x00 movq x@gottpoff(%rip), %rax 0x06 movq %fs:(%rax), %rax # %rax - contains contents of TLS variable |
R_AMD64_GOTTPOFF <none> |
x |
未完成的重定位 |
符号 |
|
GOT[n] |
R_AMD64_TPOFF64 |
x |
此代码序列实现线程局部存储的访问模型中介绍的 LE 模型。
表 8–22 x64: 局部可执行的线程局部变量的访问代码
代码序列 |
初始重定位 |
符号 |
0x00 movq %fs:0, %rax 0x06 leaq x@tpoff(%rax), %rax # %rax - contains address of TLS variable |
<none> R_AMD64_TPOFF32 |
x |
要装入 TLS 变量的内容(而不是 TLS 变量的地址),可使用以下序列。
表 8–23 x64: 局部可执行的线程局部变量的访问代码 II
代码序列 |
初始重定位 |
符号 |
0x00 movq %fs:0, %rax 0x06 movq x@tpoff(%rax), %rax # %rax - contains contents of TLS variable |
<none> R_AMD64_TPOFF32 |
x |
以下序列更短。
表 8–24 x64: 局部可执行的线程局部变量的访问代码 III
代码序列 |
初始重定位 |
符号 |
0x00 movq %fs:x@tpoff, %rax # %rax - contains contents of TLS variable |
R_AMD64_TPOFF32 |
x |
下表中列出的 TLS 重定位是针对 x64 定义的。此表中的说明使用以下表示法。
在 GOT 中分配两个连续项,以存储 TLS_index 结构。此结构将传递给 __tls_get_addr()。只能在确切的常规动态代码序列中使用此指令。
在 GOT 中分配两个连续项,以存储 TLS_index 结构。此结构将传递给 __tls_get_addr()。在运行时,将目标文件的 ti_offset 偏移字段设置为零,并初始化 ti_module 偏移。对 __tls_get_addr() 函数的调用将返回动态 TLS 块的起始偏移。只能在确切的代码序列中使用此指令。
计算变量相对于包含它的 TLS 块起始位置的偏移。计算所得的值将用作加数的立即值,并且不与特定寄存器关联。
计算包含 TLS 符号的目标文件的标识符。
在 GOT 中分配一项,以将变量偏移保存在初始 TLS 块中。此偏移相对于 TLS 块的结束位置 %fs:0。运算符只能与 movq 或 addq 指令一起使用。
计算变量相对于 TLS 块结束位置 %fs:0 的偏移。不创建任何 GOT 项。
名称 |
值 |
字段 |
计算 |
---|---|---|---|
R_AMD64_DPTMOD64 |
16 |
Word64 |
@dtpmod(s) |
R_AMD64_DTPOFF64 |
17 |
Word64 |
@dtpoff(s) |
R_AMD64_TPOFF64 |
18 |
Word64 |
@tpoff(s) |
R_AMD64_TLSGD |
19 |
Word32 |
@tlsgd(s) |
R_AMD64_TLSLD |
20 |
Word32 |
@tlsld(s) |
R_AMD64_DTPOFF32 |
21 |
Word32 |
@dtpoff(s) |
R_AMD64_GOTTPOFF |
22 |
Word32 |
@gottpoff(s) |
R_AMD64_TPOFF32 |
23 |
Word32 |
@gottpoff(s) |