翻译《ARM汇编与逆向工程 蓝狐卷》第二章 线程本地存储

原文:《Blue Fox: Arm Assembly Internals & Reverse Engineering》 Maria Markstedter—— Chapter 2: ELF File Format Internals——Thread‐Local StorageCover

主要内容由o1-mini翻译,本人订正

线程本地存储

除了全局数据变量,C 和 C++ 程序还可以定义线程本地数据变量。对于程序员来说,线程本地全局变量在大多数情况下看起来和行为上与普通的全局变量几乎完全相同,唯一的区别是它们使用 C++ 中的 __thread_local 关键字或 GNU 扩展关键字 __thread 进行注释。

与传统的全局变量不同,传统全局变量在整个程序中只有一个实例,所有线程都可以读取和写入它,而每个线程都会为自己的线程本地变量维护一个独特的存储位置。因此,对线程本地变量的读取和写入对程序中的其他线程是不可见的。

在图 2.2 中,我们可以看到程序访问线程本地变量与访问全局变量的区别。在这里,两个线程都将全局变量视为引用相同的内存地址。因此,一个线程对该变量的写操作对另一个线程是可见的,反之亦然。相比之下,两个线程看到的线程本地变量则由不同的内存地址支持。对线程本地变量的写操作不会改变程序中其他线程所看到的该变量的值。

与普通的全局变量一样,线程本地变量可以从共享库依赖中导入。例如,众所周知的 errno 变量,用于跟踪各种标准库函数的错误,是一个线程本地变量1。线程本地变量可以被零初始化或静态初始化。

Figure 2.2: Thread‐local versus global variables

图2.2:线程局部变量与全局变量

为了了解它如何工作,思考以下程序 tls.c,它定义了两个 TLS 本地变量,myThreadLocalmyUninitializedLocal

__thread int myThreadLocal = 3;
__thread int myUninitializedLocal;
int main() { return 0; }

让我们编译这个程序,并使用 readelf 查看程序的情况。

user@arm64:~$ gcc tls.c -o tls.so
user@arm64:~$ readelf -lW tls.so 
Elf file type is DYN (Shared object file)  
Entry point 0x650 
There are 10 program headers, starting at offset 64

Program Headers: 
  Type         Offset       VirtAddr    PhysAddr  FileSiz    MemSiz    Flg   Align 
  PHDR         0x000040     0x…40       0x…40     0x000230   0x000230  R     0x8 
  INTERP       0x000270     0x…270      0x…270    0x00001b   0x00001b  R     0x1 
      [Requesting program interpreter: /lib/ld-linux-aarch64.so.1] 
  LOAD         0x000000     0x…00       0x…00     0x00091c   0x00091c  R E    …
  LOAD         0x000db4     0x…10db4    0x…10db4  0x00027c   0x000284  RW     …
  DYNAMIC      0x000dc8     0x…10dc8    0x…10dc8  0x0001e0   0x0001e0  RW     0x8 
  NOTE         0x00028c     0x…28c      0x…28c    0x000044   0x000044  R      0x4 
  TLS          0x000db4     0x…10db4    0x…10db4  0x000004   0x000008  R      0x4 
  GNU_EH_FRAME 0x0007f8     0x…7f8      0x…7f8    0x000044   0x000044         … 
  GNU_STACK    0x000000     0x…00       0x…00     0x000000   0x000000  RW     0x10 
  GNU_RELRO    0x000db4     0x…10db4    0x…10db4  0x00024c   0x00024c  R      0x1 
Section to Segment mapping: 
  Segment Sections… 
   00 
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .tdata .init_array .fini_array .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .tdata .tbss  
   07     .eh_frame_hdr 
   08 
   09     .tdata .init_array .fini_array .dynamic .got

在这里,我们可以看到我们的程序现在定义了一个包含两个逻辑部分的 TLS 程序头:.tdata 和 .tbss。

程序中定义的每个线程本地变量在 ELF 文件的 TLS 表中都有一个对应的条目,该表由 TLS 程序头引用。这个条目指定了每个线程本地变量的字节大小,并为每个线程本地变量分配了一个“TLS 偏移量”,即该变量在线程本地数据区域中使用的偏移量。

您可以通过符号表查看这些变量的确切 TLS 偏移量。_TLS_MODULE_BASE 是一个符号,用于引用给定模块的线程本地存储(TLS)数据的基地址。这个符号用作给定模块的 TLS 数据的基指针,并指向包含该模块所有线程本地数据的内存区域的起始位置。$d 是一个映射符号。除了这两种特殊情况之外,我们可以看到我们的程序只包含了两个线程本地变量,其中 myThreadLocal 的 TLS 偏移量为 0,myUninitializedLocal 的 TLS 偏移量为 4。

user@arm64:~$  readelf -s a.out  | grep TLS  
    55: 0000000000000000     0 TLS     LOCAL  DEFAULT   18 $d  
    56: 0000000000000004     0 TLS     LOCAL  DEFAULT   19 $d  
    72: 0000000000000000     0 TLS     LOCAL  DEFAULT   18 _TLS_MODULE_BASE_  
    76: 0000000000000000     4 TLS     GLOBAL DEFAULT   18 myThreadLocal  
    92: 0000000000000004     4 TLS     GLOBAL DEFAULT   19 myUninitializedLocal  

如果本地变量是静态初始化的,该变量对应的 TLS 条目也会指向存储在磁盘上 ELF 文件的 .tdata 部分中的该本地变量的“初始模板”。未初始化的 TLS 条目指向 .tbss 数据部分,从而避免在 ELF 文件中存储多余的零。将这两个区域连接起来形成程序或共享库的 TLS 初始化镜像。在我们的示例中,这意味着程序的 TLS 初始化镜像将是八字节序列 03 00 00 00 00 00 00 002

线程本地存储的运行时机制可能有点复杂,但基本如下,并在图 2.3 中展示:

file

图2.3:线程本地存储的运行时机制

每个线程都可以访问一个线程指针寄存器。在 64 位 Arm 上,这个寄存器是系统的 TPIDR_EL0 寄存器,而在 32 位 Arm 上,它是系统的 TPIDRURW 寄存器3

线程指针寄存器指向为该线程分配的线程控制块(TCB)。在 64 位 Arm 上,TCB 为 16 字节,在 32 位 Arm 上为 8 字节。

紧接着 TCB 后面的是主程序二进制文件的线程本地变量,即从线程指针持有的地址开始的字节偏移 16(或在 32 位上为 8)。

主程序二进制文件的共享库依赖项的 TLS 区域随后存储。

TCB 还在 TCB 的偏移量零处维护一个指向 DTV 数组的指针。DTV 数组以一个生成字段开始,但除此之外,它是指向每个库的线程本地存储的指针数组。

使用 dlopen 在运行时加载的库相关的线程本地变量被分配在单独的存储中,但仍由 DTV 数组指向。

这种 TLS 实现方案不仅允许程序访问其自身程序模块中定义的线程本地变量,还允许访问在共享库中定义的线程本地变量。在编译时,当遇到对线程本地变量的加载或存储时,编译器将使用四种 TLS 访问模型之一来发出 TLS 访问。编译器通常根据表 2.8 中的信息选择这种模型,但也可以使用 -ftls-model 命令行选项手动覆盖,或者通过 C 和 C++ 中的 __attribute__((tls_model("name")))属性按每个变量进行覆盖4

表 2.8 描述了这些模型及其约束,表中较高的条目比表中较低的条目具有更高的运行时效率。

表 2.8: TLS 模型

TLS 模型 被编译的模块 访问的变量定义在
local-exec 主程序二进制文件 主程序二进制文件
initial-exec 任何程序二进制文件 主程序二进制文件的任何静态依赖
local-dynamic 任何程序二进制文件 定义在相同的二进制文件中
global-dynamic 任何程序二进制文件 任何程序二进制文件

Local-Exec TLS 访问模型

Local-Exec 模型是最快但最受限制的 TLS 访问模型,仅在主程序二进制文件访问其自身程序二进制文件中定义的线程局部变量时可用。

Local-Exec 模型基于以下观察:给定线程的线程指针直接指向该线程的 TCB(线程控制块),在 TCB 元数据之后是当前线程的主程序线程局部数据。对于 64 位程序,这个 TCB 元数据是 16 字节;对于 32 位程序,则是 8 字节。这意味着在 TLS 偏移量 4 处访问一个变量,例如,在 64 位系统上,将按以下方式进行:

  1. 访问当前线程的线程局部指针。
  2. 在此值上加上 16 或 8,以跳过 TCB,再加上 4,这是变量的 TLS 偏移量。
  3. 从该地址读取或写入以访问变量。

此模型仅适用于程序二进制文件。共享库无法使用此方法,主程序二进制文件也无法使用此模型访问在共享库中定义的线程局部变量。对于这些访问,必须使用其他访问模型。

Initial-Exec TLS 访问模型

Initial-Exec TLS 访问模型用于访问定义在程序初始化序列期间加载的共享库中的线程局部变量,即不是通过 dlopen 在运行时加载的共享库中的变量。这个模型有严格的要求,因此以这种方式编译的程序会在其动态节中设置 DF_STATIC_TLS 标志,以阻止通过 dlopen 加载库。

在这种情况下,程序在编译时无法确定被访问变量的 TLS 偏移量。程序通过使用 TLS 重定位来解决这种不确定性。加载器使用这种重定位来通知程序跨边界访问的变量的 TLS 偏移量。因此,在运行时,访问该变量的步骤如下:

  1. 访问线程指针。
  2. 加载由对应变量的 TLS 重定位放置在全局偏移表中的 TLS 偏移值。
  3. 将两者相加。
  4. 从该指针读取或写入以访问变量。

General-Dynamic TLS 访问模型

General-Dynamic TLS 访问模型是最通用但也是最慢的访问 TLS 变量的方式。该模型可以由任何程序模块使用,以访问在任何模块中定义的 TLS 变量,包括其自身或其他地方定义的变量。

为了实现这一过程,程序使用一个名为 __tls_get_addr 的辅助函数。该函数接受一个参数,该参数是指向包含模块 ID 和被访问变量的 TLS 偏移量的一对整数的指针,并返回该结构引用的确切线程本地地址。这些结构本身存储在程序二进制文件的全局偏移表(GOT)部分中。该结构中的模块 ID 是与我们正在运行的模块对应的 DTV 结构中的唯一索引。结构定义如下,并且在 32 位和 64 位 Arm 上相同5

typedef struct dl_tls_index
{
  unsigned long int ti_module;
  unsigned long int ti_offset;
} tls_index;

当然,自然的问题是程序在编译时如何知道变量的 TLS 模块 ID 或 TLS 偏移量。对于我们自己程序二进制文件中的线程局部变量,TLS 偏移量可能是已知的,但对于外部符号,这在运行时之前是无法知道的。

为了解决这个问题,ELF 文件重新利用了重定位。可能的重定位类型非常多,但表 2.9 中显示的重定位类型基本上展示了这一过程的工作原理。

表 2.9: Arm ELF 文件的基本 TLS 重定位类型

TLS 重定位类型 含义
R_ARM_TLS_DTPMOD32 写入与重定位指定符号对应的模块 ID(如果符号为空,则写入被加载模块的模块 ID)。
R_AARCH64_TLS_DTPMOD 写入与重定位指定符号对应的模块 ID(如果符号为空,则写入被加载模块的模块 ID)。
R_ARM_TLS_DTPOFF32 写入与重定位指定符号对应的 TLS 偏移量。
R_AARCH64_TLS_DTPOFF 写入与重定位指定符号对应的 TLS 偏移量。
R_ARM_TLS_TPOFF32 写入从线程指针地址计算的偏移量,对应于重定位指定的符号。注意,这仅在模块始终在程序加载期间加载而非通过 dlopen 加载时有效。
R_AARCH64_TLS_TPOFF 写入从线程指针地址计算的偏移量,对应于重定位指定的符号。注意,这仅在模块始终在程序加载期间加载而非通过 dlopen 加载时有效。

__tls_get_addr 函数执行以下操作,用伪代码描述如下6

void* __tls_get_addr(struct dl_tls_index* tlsentry)
{
  // 获取线程指针:
  tcbhead_t* tp = (tcbhead_t*)__builtin_thread_pointer();

  // 检查线程的 DTV 版本,并在必要时更新:
  dtv_t* dtv = tp->dtv;
  if (dtv[0].counter != dl_tls_generation)
    update_dtv_list();

  // 分配 TLS 条目 
  uint8_t* tlsbase = (uint8_t*)dtv[tlsentry->ti_module].pointer.val;
  if (tlsbase == NULL)
    return allocate_tls_section_for_current_thread(tlsentry->ti_module);
  return tlsbase + tlsentry->ti_module;
}

DTV 版本检查的目的是处理一种情况:一个线程通过 dlopen 动态打开一个共享库,另一个线程随后尝试访问该共享库中的线程局部变量。这避免了在 dlopen 期间需要暂停所有线程并动态调整它们各自的 DTV 数组。在 dlopendlclose 期间,全局 DTV 版本会被更新。线程将在下一次调用 __tls_get_addr 时更新自己的 DTV 数组,释放与现已关闭的共享库相关联的任何线程本地存储,并确保 DTV 数组本身足够长,以包含每个打开的共享库的条目。

延迟 TLS 区段分配的目的是作为一种小的性能优化。这确保了线程仅为实际使用这些变量的动态打开的共享库的线程局部变量分配内存。

这一过程的总体结果是,编译器发出通过调用 __tls_get_addr 访问线程局部变量的代码。加载器使用重定位来传达被访问变量的模块 ID 和 TLS 偏移量。最后,运行时系统使用 __tls_get_addr 函数本身按需分配线程本地存储,并将线程局部变量的地址返回给程序。

Local-Dynamic TLS 访问模型

Local-Dynamic TLS 访问模型由需要访问其自身线程局部变量的共享库使用,无论这些共享库是静态加载还是动态加载,实际上它是 Global-Dynamic TLS 访问模型的简化形式。该模型基于以下观察:在访问其自身线程局部变量时,程序已经知道这些变量在其自身 TLS 区域内的偏移量;它唯一不知道的是该 TLS 区域的确切位置。

针对这种情况,编译器有时可以发出稍快的指令序列。假设一个程序试图连续访问两个线程局部变量,一个偏移量为 16,另一个偏移量为 256。编译器不会发出两次调用 __tls_get_addr,而是发出一次对当前线程调用 __tls_get_addr,传递当前模块 ID 和偏移量 0,以获取当前模块自身线程的 TLS 地址。然后加上 16 得到第一个变量的地址,再加上 256 得到第二个变量的地址。

Notes

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇