如无特殊说明,系统为linux,架构为x86 32bit,使用glibc,通过libhybris调用android bionic的驱动。android版本5.1.0_r1。
一、什么是TLS
TLS的全称是Thread Local Storage,是指进程中每一个线程都独有的变量,名字相同,但是读写互不影响。最常见的TLS之一就是errno,每一个线程都有自己的errno,保存着该线程的最近一次函数调用错误原因,别的线程干啥都不会影响到这个线程的errno,防止别的线程覆盖该线程的errno。
PS:
tid是线程的id,保证同一个进程中是不重复的,但是不同进程之间可以重复。
真想修改其他线程的TLS也可以,glibc中获取其他线程的tid,强制转换为struct pthread结构体,就可以干很多事了。
1、如何使用TLS
声明变量时,添加关键字__thread(glibc支持,android bionic不支持),或者通过pthread_key_create, pthread_setspecific和pthread_getspecific三个函数去申请和读写TLS:
__thread int x = 3;
printf("%d\n", x);
pthread_key_t key;
pthread_key_create(&key, NULL);
pthread_setspecific(key,"hello world");
printf("%s\n", pthread_getspecific(key));
2、TLS的原理
linux内核对线程进行切换时,会保存和恢复一些寄存器,这是操作系统的基础知识。
有一个比较特殊的寄存器,叫做gs,没见过的话也没事,它和cs,ds,es,ss差不多,都是段寄存器。
只是CPU厂商并没有规定gs的作用,可以由操作系统自己发挥,与此类似的还有fs寄存器。
先说明下保护模式和实模式下段寄存器的含义是不同的。
实模式,也就是古老的dos时期的那种东西,地址总线16根,最大访问空间1M的。cs:ip表示的地址就是cs*16+ip。
保护模式,现在的cpu为了兼容老东西,开机时是实模式的,然后打开A20,以及其他的一些东西,就进入了保护模式。
保护模式下的段寄存器,我觉得叫做选择符更形象些,它本身并不保存真正的地址信息,而保存了一个索引,一个描述表选择,一个特权级。
比如gs=0x33,需要按照二进制来看
high low
110 0 11
idx gdt/ldt privilege
最低位的11b,也就是3,表示特权级,一般内核为特权级0,用户态为3。
最高位的110b,也就是6,表示在gdt或者ldt中的下标。
中间的0表示使用gdt,如果为1,表示使用ldt。
那么什么是gdt和ldt呢?
gdt是全局描述符表,ldt是局部描述符表。他们都是表格,表中的每项都包含了一个地址,以及其他一些东西。
gdt就是系统全局的一个表,每个线程都会在gdt中占据一些位置,用于存放线程的tss和ldt地址。当然gdt中还有其他的东西。
每个线程都有自己的ldt,存放线程自己的一些信息,比如数据段和代码段的地址。
比如gs=0x33时,gs:4指的就是gdt[6]中的地址,加上偏移量4。
linux内核中有个set_thread_area系统调用,就是用来设置线程的gs寄存器以及对应的gdt描述符的内容:
int do_set_thread_area(struct task_struct *p, int idx,
struct user_desc __user *u_info,
int can_allocate)
{
struct user_desc info;
if (copy_from_user(&info, u_info, sizeof(info)))
return -EFAULT;
if (!tls_desc_okay(&info))
return -EINVAL;
if (idx == -1)
idx = info.entry_number;
/*
* index -1 means the kernel should try to find and
* allocate an empty descriptor:
*/
if (idx == -1 && can_allocate) {
idx = get_free_idx();
if (idx < 0)
return idx;
if (put_user(idx, &u_info->entry_number))
return -EFAULT;
}
if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
return -EINVAL;
set_tls_desc(p, idx, &info, 1);
return 0;
}
glibc或者bionic都会调用set_thread_area来设置线程的数据的,bionic是通过__set_tls来调用的。
其实线程有很大一部分是在glibc/bionic中