gcc-glibc如何实现线程私有变量

一、C库对于fs值的分配

glibc-2.11\nptl\sysdeps\x86_64\tls.h
线程创建时的逻辑,可以看到是执行的ARCH_SET_FS接口设置的
/* Code to initially initialize the thread pointer. This might need
special attention since 'errno' is not yet available and if the
operation can cause a failure 'errno' must not be touched.

We have to make the syscall for both uses of the macro since the
address might be (and probably is) different. */
# define TLS_INIT_TP(thrdescr, secondcall) \
({ void *_thrdescr = (thrdescr); \
tcbhead_t *_head = _thrdescr; \
int _result; \
\
_head->tcb = _thrdescr; \
/* For now the thread descriptor is at the same address. */ \
_head->self = _thrdescr; \
\
/* It is a simple syscall to set the %fs value for the thread. */ \
asm volatile ("syscall" \
: "=a" (_result) \
: "0" ((unsigned long int) __NR_arch_prctl), \
"D" ((unsigned long int) ARCH_SET_FS), \
"S" (_thrdescr) \
: "memory", "cc", "r11", "cx"); \
\
_result ? "cannot set %fs base address for thread-local storage" : 0; \
})

arch_prctl函数的man手册说明
Subfunctions for x86-64 are:

ARCH_SET_FS
Set the 64-bit base for the FS register to addr.

结构,对应的x86_64系统下,pthread_self的实现
/* Return the thread descriptor for the current thread.

The contained asm must *not* be marked volatile since otherwise
assignments like
pthread_descr self = thread_self();
do not get optimized away. */
# define THREAD_SELF \
({ struct pthread *__self; \
asm ("movq %%fs:%c1,%q0" : "=r" (__self) \
: "i" (offsetof (struct pthread, header.self))); \
__self;})

二、fs、gs前缀意味着什么

在早期,这些s是segment的缩写,而cs、ds就是code segment,data segment的缩写。那么,这些寄存器中存储的是什么内容呢?这些寄存器中存储的是一个16bits,有一个bit表示引用的是GDT还是LDT,还有13个bits组成一个数值,索引项GDT或者LDT这个表格的下标,也就是一个数组的下标。这个LDT和GDT中存储了一些权限信息,当然对于我们来说比较关心的就是基地址了。当通过%fs:offset访问内存的时候,其实是通过fs寄存器找到GDT/LDT表格对应项,从中取出基地址,然后加上offset获得最终访问地址。
那么LDT/GDT的内容从哪里来?这个是需要操作系统按照指定的格式创建table(特定格式的数组),然后通过lgdt、lldt来加载:
LGDT/LIDT—Load Global/Interrupt Descriptor Table Register
LLDT—Load Local Descriptor Table Register
在386系统中,这些线程私有的变量通过的寄存器引用是存放在GDT中,在每次进程切换的时候由操作系统进行切换。

三、操作系统在进程切换时的处理

其中loadsegment(fs, next->fsindex)更新fs寄存器本身的值,wrmsrl(MSR_FS_BASE, next->fs)更新fs指向的GDT表中对应项的内容
linux-2.6.21\arch\x86_64\kernel\process.c
__kprobes struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
……
/*
* Switch FS and GS.
*/
{
unsigned fsindex;
asm volatile("movl %%fs,%0" : "=r" (fsindex));
/* segment register != 0 always requires a reload.
also reload when it has changed.
when prev process used 64bit base always reload
to avoid an information leak. */
if (unlikely(fsindex | next->fsindex | prev->fs)) {
loadsegment(fs, next->fsindex);
/* check if the user used a selector != 0
* if yes clear 64bit base, since overloaded base
* is always mapped to the Null selector
*/
if (fsindex)
prev->fs = 0;
}
/* when next process has a 64bit base use it */
if (next->fs)
wrmsrl(MSR_FS_BASE, next->fs);
prev->fsindex = fsindex;
}
{
unsigned gsindex;
asm volatile("movl %%gs,%0" : "=r" (gsindex));
if (unlikely(gsindex | next->gsindex | prev->gs)) {
load_gs_index(next->gsindex);
if (gsindex)
prev->gs = 0;
}
if (next->gs)
wrmsrl(MSR_KERNEL_GS_BASE, next->gs);
prev->gsindex = gsindex;
}
……
}

四、静态tls的分配

这个通过汇编代码可以看到,所有的线程私有变量放入单独的section中,并且通过线程指针减去一个地址获得这个变量的位置。也就是说在pthread结构前面有一个隐藏的静态tls结构。
tsecer@harry: cat thread_local.c
thread_local int x;
int foo()
{
return x;
}
tsecer@harry: g++ -std=c++11 -c thread_local.c
tsecer@harry: readelf -r thread_local.o

重定位节 '.rela.text' 位于偏移量 0x5a8 含有 1 个条目:
Offset Info Type Sym. Value Sym. Name + Addend
000000000008 000900000017 R_X86_64_TPOFF32 0000000000000000 x + 0

重定位节 '.rela.eh_frame' 位于偏移量 0x5c0 含有 1 个条目:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
tsecer@harry: objdump -d thread_local.o

thread_local.o: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <_Z3foov>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 64 8b 04 25 00 00 00 mov %fs:0x0,%eax
b: 00
c: 5d pop %rbp
d: c3 retq
tsecer@harry:
可以看到,默认使用的是fs寄存器作为selector。

其中R_X86_64_TPOFF32重定位类型的说明:
#define R_X86_64_TPOFF32 23 /* Offset in initial TLS block */

所有的线程私有放在单独节中:

tsecer@harry: readelf -S thread_local.o
共有 13 个节头,从偏移量 0x118 开始:

节头:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000000e 0000000000000000 AX 0 0 4
[ 2] .rela.text RELA 0000000000000000 000005a8
0000000000000018 0000000000000018 11 1 8
[ 3] .data PROGBITS 0000000000000000 00000050
0000000000000000 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 00000050
0000000000000000 0000000000000000 WA 0 0 4
[ 5] .tbss NOBITS 0000000000000000 00000050
0000000000000004 0000000000000000 WAT 0 0 4
[ 6] .comment PROGBITS 0000000000000000 00000050

可执行文件中tls节

tsecer@harry: readelf -l a.out

Elf 文件类型为 EXEC (可执行文件)
入口点 0x4006b0
共有 10 个程序头,开始于偏移量64

程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x0000000000000230 0x0000000000000230 R E 8
INTERP 0x0000000000000270 0x0000000000400270 0x0000000000400270
0x000000000000001c 0x000000000000001c R 1
[正在请求程序解释器:/lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000ab4 0x0000000000000ab4 R E 200000
LOAD 0x0000000000000dcc 0x0000000000600dcc 0x0000000000600dcc
0x0000000000000280 0x000000000000029c RW 200000
DYNAMIC 0x0000000000000de8 0x0000000000600de8 0x0000000000600de8
0x0000000000000210 0x0000000000000210 RW 8
NOTE 0x000000000000028c 0x000000000040028c 0x000000000040028c
0x0000000000000044 0x0000000000000044 R 4
TLS 0x0000000000000dcc 0x0000000000600dcc 0x0000000000600dcc
0x0000000000000000 0x0000000000000004 R 4
GNU_EH_FRAME 0x0000000000000964 0x0000000000400964 0x0000000000400964
0x000000000000003c 0x000000000000003c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000dcc 0x0000000000600dcc 0x0000000000600dcc
0x0000000000000234 0x0000000000000234 R 1

Section to Segment mapping:
段节...
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 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .tbss
07 .eh_frame_hdr
08
09 .init_array .fini_array .jcr .dynamic .got
tsecer@harry:

五、动态tls的分配

动态通过pthread_key_create、pthread_setspecific函数完成,由于每个线程有自己的控制块,所以可以在控制块中动态分配内存。为了相同的key在不同的进程中能够有一致性,所以pthread_key_create相当于动态分配一个统一的下标,本质上就是把静态连接中的偏移量统一为一个运行时动态确定的偏移量。

 

posted on 2020-05-19 20:46  tsecer  阅读(1119)  评论(0编辑  收藏  举报

导航