title: TLS
date: 2024-07-31 13:43:46
tags:
- “PWN”
- “Linux”

基本概念

线程的访问非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址,那么这就是很少见的情况),但实际运用中线程也拥有自己的私有存储空间,包括以下几方面:

  • 栈(尽管并非完全无法被其他线程访问,但一般情况下仍然可以认为是私有的数据)。
  • 线程局部存储(Thread Local Storage, TLS)。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量。
  • 寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此为线程私有。

实际上,线程私有的数据有:

  • 局部变量(栈、寄存器)
  • 函数的参数(栈、寄存器)
  • TLS 数据(线程局部存储)

线程共享的数据有:

  • 全局变量
  • 堆上的数据
  • 函数里的静态变量
  • 程序代码,任何线程都有有权利读取并执行任何代码。
  • 打开的文件,A 线程打开的文件可以由 B 线程读写。

一个全局变量如果使用 __thread 关键字修饰,那么这个变量就变成线程私有的 TLS 数据,也就是说每个线程都在自己所属 TLS 中单独保存一份这个变量的副本。例如下面的代码中,ab 都是 TLS 数据,而 c 是全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// gcc tls.c -o tls -g -pthread
#include <pthread.h>
#include <stdio.h>
#include <stdint-gcc.h>

__thread uint32_t a = 0x114514;
__thread uint32_t b;
uint32_t c = 0x1919810;

void *thread(void *arg) {
printf("thread: a(%p) = %x, b(%p) = %x, c(%p) = %xn", &a, a, &b, b, &c, c);
return NULL;
}

int main(void) {
a = 0x12345678;
b = 0x87654321;
c = 0xdeadbeef;
printf("thread: a(%p) = %x, b(%p) = %x, c(%p) = %xn", &a, a, &b, b, &c, c);
pthread_t pid;
pthread_create(&pid, NULL, thread, NULL);
pthread_join(pid, NULL);
return 0;
}
/*
thread: a(0x7f1ec78f0738) = 12345678, b(0x7f1ec78f073c) = 87654321, c(0x562d7468a010) = deadbeef
thread: a(0x7f1ec70ed6f8) = 114514, b(0x7f1ec70ed6fc) = 0, c(0x562d7468a010) = deadbeef
*/

分析生成的 ELF 文件的节表,发现多出了 .tdata.tbss ,这两个节分别记录已初始化和未初始化的 TLS 数据。

其中 .tbss 在 ELF 文件中不占用空间, .tdata 在 ELF 中存储了初始化的数据,比如上面的代码中的 __thread uint32_t a = 0x114514

ELF 加载到内存中后, .tdata.tbss 这两个节合并为一个段,在程序头表中这个段的 p_typePT_TLS(7)

TLS(Thread Local Storage)的结构与 TCB(Thread Control Block)以及 dtv(dynamic thread vector)密切相关,每一个线程中每一个使用了 TLS 功能的模块都拥有一个 TLS Block 。这几者的关系如下图所示:

image-20240220150935106

注意,这里是 x86_64-ABI 要求的 TLS 结构,Glibc 实现的 TLS 结构与上图有一些差异。

根据图中显示的信息,TLS Blocks 可以分为两类:

  • 一类是程序装载时就已经存在的(位于 TCB 前),这一部分 Block 被称为 _static TLS_
  • 一类是右边的 Blocks 是动态分配的,它们被使用 dlopen 函数在程序运行时动态装载的模块所使用。

TCB 作为线程控制块,保存着 dtv 数组的入口,dtv 数组中的每一项都是 TLS Block 的入口,它们是指向 TLS Blocks 的指针。特别的,dtv 数组的第一个成员是一个计数器,每当程序使用 dlopen 函数或者 dlfree 函数加载或者卸载一个具备 TLS 变量的模块,该计数器的值都会加一,从而保证程序内版本的一致性。 特别的,ELF 文件本身对应的 TLS Block 一定在 dtv 数组中占据索引为 1 的位置,且位置上与 TCB 相邻。 还需要注意的是,图中出现了一个名为 ���tpt 的指针,在 i386 架构上,这个指针为 gs 段寄存器;在 x86_64 架构上,该指针为 fs 段寄存器。由于该指针与 ELF 文件本身对应的 TLS Block 之间的偏移是固定的,程序在编译时就可以将 ELF 中线程变量的地址(偏移)硬编码到目标文件中。

主线程 TLS 初始化

前面提到过在 main 开始前会调用 __libc_setup_tls 初始化 TLS 。

__libc_setup_tls 函数中,首先会遍历 ELF 的程序头表,找到 p_typePT_TLS(7) 的段,这个段中就存储着 TLS 的初始化数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Look through the TLS segment if there is any.  */
if (_dl_phdr != NULL)
for (phdr = _dl_phdr; phdr < &_dl_phdr[_dl_phnum]; ++phdr)
if (phdr->p_type == PT_TLS) {
/* Remember the values we need. */
memsz = phdr->p_memsz;
filesz = phdr->p_filesz;
initimage = (void *) phdr->p_vaddr + main_map->l_addr;
align = phdr->p_align;
if (phdr->p_align > max_align)
max_align = phdr->p_align;
break;
}

然后通过 brk 调用为 TLS 中的数据以及一个 pthread 结构体分配内存。其中 pthread 结构体的第一项为 tcbhead_t header; ,即前面提到的 TCB

1
2
3
4
5
/* Align the TCB offset to the maximum alignment, as
_dl_allocate_tls_storage (in elf/dl-tls.c) does using __libc_memalign
and dl_tls_static_align. */
tcb_offset = roundup (memsz + GLRO(dl_tls_static_surplus), max_align);
tlsblock = __sbrk(tcb_offset + TLS_INIT_TCB_SIZE + max_align);

tcbhead_t 结构体定义如下,也就是很多资料中提到的 TLS 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
unsigned long int vgetcpu_cache[2];
# ifndef __ASSUME_PRIVATE_FUTEX
int private_futex;
# else
int __glibc_reserved1;
# endif
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
long int __glibc_reserved2;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));

void *__padding[8];
} tcbhead_t;

之后初始化 _dl_static_dtv ,也就是前面提到的 dtv 数组,具体过程为:

  • tlsblock 地址关于 max_align 向上对齐。
  • _dl_static_dtv[0].counter 初始化为 dtv 的数量,由于 _dl_static_dtv 前两项分别用于记录 dtv 总数和使用的数量,因此这里记录的 dtv 数量是要减去这两项的。
  • _dl_static_dtv[1].counter 初始化为 0 。
  • _dl_static_dtv[2] 也就是当前模块对应的 dtvpointer.val 指向 TLS 。
  • _dl_static_dtv[2].pointer.to_free 置为 NULL 。
  • 将 TLS 的初始数据也就是 PT_TLS 段中的数据复制到 TLS 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct dtv_pointer
{
void *val; /* Pointer to data, or TLS_DTV_UNALLOCATED. */
void *to_free; /* Unaligned pointer, for deallocation. */
};

/* Type for the dtv. */
typedef union dtv
{
size_t counter;
struct dtv_pointer pointer;
} dtv_t;

/* Number of additional entries in the slotinfo array of each slotinfo
list element. A large number makes it almost certain take we never
have to iterate beyond the first element in the slotinfo list. */
#define TLS_SLOTINFO_SURPLUS (62)
dtv_t _dl_static_dtv[2 + TLS_SLOTINFO_SURPLUS];

/* Align the TLS block. */
tlsblock = (void *) (((uintptr_t) tlsblock + max_align - 1)
& ~(max_align - 1));

/* Initialize the dtv. [0] is the length, [1] the generation counter. */
_dl_static_dtv[0].counter = (sizeof(_dl_static_dtv) / sizeof(_dl_static_dtv[0])) - 2;
// _dl_static_dtv[1].counter = 0; would be needed if not already done

/* Initialize the TLS block. */
_dl_static_dtv[2].pointer.val = ((char *) tlsblock + tcb_offset
- roundup (memsz, align ?: 1));
_dl_static_dtv[2].pointer.to_free = NULL;
/* sbrk gives us zero'd memory, so we don't need to clear the remainder. */
memcpy(_dl_static_dtv[2].pointer.val, initimage, filesz);

此时 TLS 相关结构之间的关系如下图所示:

图片描述

另外还会初始化 link_map 中的 TLS 相关的数据,由此我们可以知道 link_map 中这些字段的含义:

  • l_tls_offset :TLS 数据相对于 TCB 的偏移。
  • l_tls_align:TLS 初始数据的对齐,在 TLS 中 TLS 初始数据关于 l_tls_align 向上取整。
  • l_tls_blocksize:TLS 初始数据的大小,也就是前面提到的 TLS Block 的大小。
  • l_tls_initimage:TLS 初始数据的地址。也就是 PT_TLS 段的地址。
  • l_tls_initimage_sizePT_TLS 段在文件中的大小,也就是 .tdata 的大小。
  • l_tls_modid:模块编号(dtv 中的下标)。
1
2
3
4
5
6
7
8
9
struct link_map *main_map = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
main_map->l_tls_offset = roundup (memsz, align ?: 1);
/* Update the executable's link map with enough information to make
the TLS routines happy. */
main_map->l_tls_align = align;
main_map->l_tls_blocksize = memsz;
main_map->l_tls_initimage = initimage;
main_map->l_tls_initimage_size = filesz;
main_map->l_tls_modid = 1;

创建线程时 TLS 初始化

创建线程的函数 pthread_create 实际调用的是 __pthread_create_2_1 函数,在该函数中调用了 allocate_stack 函数。

1
2
3
4
# define ALLOCATE_STACK(attr, pd) allocate_stack (attr, pd, &stackaddr)

struct pthread *pd = NULL;
int err = ALLOCATE_STACK (iattr, &pd);

allocate_stack 函数中会调用 mmap 为线程分配栈空间,然后初始化栈底为一个 pthread 结构体并将指针 pd 指向该结构体。最后调用 _dl_allocate_tls 函数为 TCB 创建 dtv 数组。

1
2
3
4
5
6
7
8
  struct pthread *pd;
...
mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
...
pd = (struct pthread *) ((((uintptr_t) mem + size) - TLS_TCB_SIZE) & ~__static_tls_align_m1);
...
_dl_allocate_tls (TLS_TPADJ (pd))

_dl_allocate_tls 函数依次调用 allocate_dtv_dl_allocate_tls_init 分配和初始化 dtv 数组。

1
2
3
4
5
6
7
void *
_dl_allocate_tls (void *mem)
{
return _dl_allocate_tls_init (mem == NULL
? _dl_allocate_tls_storage ()
: allocate_dtv (mem));
}

allocate_dtv 函数调用了 calloc 函数为 dtv 数组分配内存,初始化 dtv[0].counter 为数组中元素数量,并且让 pd->dtv 指向 dtv[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* Install the dtv pointer.  The pointer passed is to the element with
index -1 which contain the length. */
# define INSTALL_DTV(descr, dtvp)
((tcbhead_t *) (descr))->dtv = (dtvp) + 1

static void *
allocate_dtv (void *result)
{
dtv_t *dtv;
size_t dtv_length;

/* We allocate a few more elements in the dtv than are needed for the
initial set of modules. This should avoid in most cases expansions
of the dtv. */
dtv_length = GL(dl_tls_max_dtv_idx) + DTV_SURPLUS;
dtv = calloc (dtv_length + 2, sizeof (dtv_t));
if (dtv != NULL)
{
/* This is the initial length of the dtv. */
dtv[0].counter = dtv_length;

/* The rest of the dtv (including the generation counter) is
Initialize with zero to indicate nothing there. */

/* Add the dtv to the thread data structures. */
INSTALL_DTV (result, dtv);
}
else
result = NULL;

return result;
}

_dl_allocate_tls_init 函数会遍历 dl_tls_dtv_slotinfo_list 中的 link_map ,初始化 dtv 数组并将初始数据复制到 TLS 变量中。从这里可以看出,如果一个模块有 TLS 变量,则该模块对应的 dtv->pointer.val 指向 TLS 变量的起始地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dtv[map->l_tls_modid].pointer.val = TLS_DTV_UNALLOCATED;
dtv[map->l_tls_modid].pointer.to_free = NULL;

if (map->l_tls_offset == NO_TLS_OFFSET
|| map->l_tls_offset == FORCED_DYNAMIC_TLS_OFFSET)
continue;

/* Set up the DTV entry. The simplified __tls_get_addr that
some platforms use in static programs requires it. */
dtv[map->l_tls_modid].pointer.val = dest;

/* Copy the initialization image and clear the BSS part. */
memset(__mempcpy (dest, map->l_tls_initimage,
map->l_tls_initimage_size), '�',
map->l_tls_blocksize - map->l_tls_initimage_size);

回到 __pthread_create_2_1 函数,在完成了 pthread 的一系列初始化后调用了 THREAD_COPY_STACK_GUARDTHREAD_COPY_POINTER_GUARD 两个宏,这两个宏的展开如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
((pd)->header.stack_guard = ({
__typeof(({
struct pthread *__self;
asm("mov %%fs:%c1,%0":"=r"(__self):"i"(((size_t) (&(((struct pthread *) 0)->header.self)))));
__self;
})->header.stack_guard) __value;
_Static_assert(sizeof(__value) == 1 || sizeof(__value) == 4 || sizeof(__value) == 8, "size of per-thread data");
if (sizeof(__value) == 1)asm volatile("movb %%fs:%P2,%b0":"=q"(__value):"0"(0), "i"(((size_t) (&(((struct pthread *) 0)->header.stack_guard))))); else if (sizeof(__value) == 4)asm volatile("movl %%fs:%P1,%0":"=r"(__value):"i"(((size_t) (&(((struct pthread *) 0)->header.stack_guard))))); else { asm volatile("movq %%fs:%P1,%q0":"=r"(__value):"i"(((size_t) (&(((struct pthread *) 0)->header.stack_guard))))); }
__value;
}))

((pd)->header.pointer_guard = ({
__typeof(({
struct pthread *__self;
asm("mov %%fs:%c1,%0":"=r"(__self):"i"(((size_t) (&(((struct pthread *) 0)->header.self)))));
__self;
})->header.pointer_guard) __value;
_Static_assert(sizeof(__value) == 1 || sizeof(__value) == 4 || sizeof(__value) == 8, "size of per-thread data");
if (sizeof(__value) == 1)asm volatile("movb %%fs:%P2,%b0":"=q"(__value):"0"(0), "i"(((size_t) (&(((struct pthread *) 0)->header.pointer_guard))))); else if (sizeof(__value) == 4)asm volatile("movl %%fs:%P1,%0":"=r"(__value):"i"(((size_t) (&(((struct pthread *) 0)->header.pointer_guard))))); else { asm volatile("movq %%fs:%P1,%q0":"=r"(__value):"i"(((size_t) (&(((struct pthread *) 0)->header.pointer_guard))))); }
__value;
}))

不难看出这两个宏把当前线程(当前 fs 寄存器还没有指向新线程的 TCB)的 TLS 中的 stack_guardpointer_guard 都复制到子线程的 TLS 的对应位置上。因此可以确定线程的 stack_guardpointer_guard 与主线程相同。

最后需要确定是 fs 寄存器何时被修改,因为 fs 寄存器不能再用户态修改,因此一定是一个系统调用完成了对 fs 寄存器的修改。

通过调试发现,pthread_create->create_thread->clone 中的 clone 系统调用完成了对 fs 寄存器的修改。