比如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中实现的,不全是在内核中的。
上述代码中idx和entry_number表示gs指向gdt中的第几个描述符,如果上层调用者没有指定idx和entry_number的话,由内核自己动态分配。
x86可能的值为6,7,8,x86_64可能的值为12,13,14。但是一般来说,x86上的gs的值是0x33,对应的idx为6,使用gdt中的第6个描述符。
线程切换时,修改的是gdt[6]中的东西,不会去修改gs的,gdt[6]指向了什么东西呢?glibc时gdt[6]中的是线程的tcbhead_t的指针,bionic时gdt[6]是一个指向TLS数组的指针。
线程通过gs,找到gdt中的描述符,然后找到tcbhead_t *或者TLS数组,然后在glibc和bionic中使用不同的实现,可以获得线程的tid,errno,TLS,等等信息。
==========================================================================
设置gs的值,以及gdt[6]中的地址,是在如下代码中进行设置的:
-
/* 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. */
-
# define TLS_INIT_TP(thrdescr) \
-
({ void *_thrdescr = (thrdescr); \
-
tcbhead_t *_head = _thrdescr; \
-
union user_desc_init _segdescr; \
-
int _result; \
-
\
-
_head->tcb = _thrdescr; \
-
/* For now the thread descriptor is at the same address. */ \
-
_head->self = _thrdescr; \
-
/* New syscall handling support. */ \
-
INIT_SYSINFO; \
-
\
-
/* Let the kernel pick a value for the 'entry_number' field. */ \
-
tls_fill_user_desc (&_segdescr, -1, _thrdescr); \
-
\
-
/* Install the TLS. */ \
-
INTERNAL_SYSCALL_DECL (err); \
-
_result = INTERNAL_SYSCALL (set_thread_area, err, 1, &_segdescr.desc); \
-
/*........*/
tls_fill_user_desc第二个参数为-1,表示动态申请gdt中的位置,一般为6,所以gs=0x33。
在_segdescr中保存了tcbhead_t的地址,后续在set_thread_area系统调用里将tcbhead_t的地址写到了gdt[6]中。
-
typedef struct
-
{
-
void *tcb; // 指向tcbhead_t自己
-
dtv_t *dtv; // 指向dtv数据,用于__thread类型的TLS的实现
-
void *self; // 指向struct pthread结构体
-
int multiple_threads;
-
uintptr_t sysinfo; // 快速系统调用时的入口
-
uintptr_t stack_guard;
-
uintptr_t pointer_guard;
-
int gscope_flag;
-
#ifndef __ASSUME_PRIVATE_FUTEX
-
int private_futex;
-
#else
-
int __glibc_reserved1;
-
#endif
-
/* Reservation of some values for the TM ABI. */
-
void *__private_tm[4];
-
/* GCC split stack support. */
-
void *__private_ss;
-
} tcbhead_t;
如何去获得上述代码保存的tcbhead_t,以pthread_self的实现为例:
-
# define THREAD_SELF \
-
({ struct pthread *__self; \
-
asm ("movl %%gs:%c1,%0" : "=r" (__self) \
-
: "i" (offsetof (struct pthread, header.self))); \
-
__self;})
PS:x86时,pthread结构体的第一个元素就是tcbhead_t,所以他们的地址相同:
-
struct pthread
-
{
-
union
-
{
-
#if !TLS_DTV_AT_TP
-
/* This overlaps the TCB as used for TLS without threads (see tls.h). */
-
tcbhead_t header;
-
#else
-
/*........*/
glibc中的TLS,可以分为两类,三种。
第一类通过dtv_t *dtv实现,这是一个数组,数组里面每一项都是dtv_t联合体。
-
typedef union dtv
-
{
-
size_t counter;
-
struct
-
{
-
void *val;
-
bool is_static;
-
} pointer;
-
} dtv_t;
dtv[-1]为申请的数组的大小,dtv[0]是max generation number,不知道表示什么。这两个都是counter类型的,之后的都是pointer类型的。
每个pointer类型的dtv_t联合体,都和一个被打开的有__thread变量的.so相关(dtv[1]除外,表示程序本身)。其val指向一个数组,也就是该.so中的保存所有__thread变量的一段连续空间。dtv数组的下标是l_tls_modid,表示被打开的有__thread变量的.so的序号。
保存__thread变量的连续空间的大小在编译时就确定好了,已初始化的__thread保存在.tdata段,未初始化的__thread保存在.tbss段,类似于.data和.bss的概念。
可以readelf -S 看看.tdata和.tbss的信息。
pointer类型的dtv_t联合体有静态和动态两种。
在线程创建之前被打开的.so对应的dtv_t是静态的,具体的位置在tcbhead_t前面的内存中。
在线程创建后被dlopen打开的.so对应的dtv_t是动态的,动态申请内存,具体位置在线程栈中。
gdb调试验证可以看:http://codemacro.com/2014/10/07/pthread-tls-bug/
第二类的实现在struct pthread中:
-
struct pthread
-
{
-
tcbhead_t header;
-
/*......*/
-
/* We allocate one block of references here. This should be enough
-
to avoid allocating any memory dynamically for most applications. */
-
struct pthread_key_data
-
{
-
/* Sequence number. We use uintptr_t to not require padding on
-
32- and 64-bit machines. On 64-bit machines it helps to avoid
-
wrapping, too. */
-
uintptr_t seq;
-
/* Data pointer. */
-
void *data;
-
} specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];
-
/* Two-level array for the thread-specific data. */
-
struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];
-
/*......*/
-
}
specific是一个二维数组,specific_1stblock是第一个一维数组,用于加快访问速度的。
通过pthread_key_create, pthread_setspecific和pthread_getspecific三个函数来折腾。
以pthread_getspecific来看怎么找到specific(gs—>gdt6—>tcbhead_t—>self—>THREAD_SELF—>specific)和使用二维数组的,比较简单:
-
void *
-
__pthread_getspecific (key)
-
pthread_key_t key;
-
{
-
struct pthread_key_data *data;
-
/* Special case access to the first 2nd-level block. This is the
-
usual case. */
-
if (__glibc_likely (key < PTHREAD_KEY_2NDLEVEL_SIZE))
-
data = &THREAD_SELF->specific_1stblock[key];
-
else
-
{
-
/* Verify the key is sane. */
-
if (key >= PTHREAD_KEYS_MAX)
-
/* Not valid. */
-
return NULL;
-
unsigned int idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE;
-
unsigned int idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE;
-
/* If the sequence number doesn't match or the key cannot be defined
-
for this thread since the second level array is not allocated
-
return NULL, too. */
-
struct pthread_key_data *level2 = THREAD_GETMEM_NC (THREAD_SELF,
-
specific, idx1st);
-
if (level2 == NULL)
-
/* Not allocated, therefore no data. */
-
return NULL;
-
/* There is data. */
-
data = &level2[idx2nd];
-
}
-
void *result = data->data;
-
if (result != NULL)
-
{
-
uintptr_t seq = data->seq;
-
if (__glibc_unlikely (seq != __pthread_keys[key].seq))
-
result = data->data = NULL;
-
}
-
return result;
-
}
===========================================================================
设置gs的值,以及gdt[6]中的地址,是在如下代码中进行设置的:
-
void __libc_init_tls(KernelArgumentBlock& args) {
-
__libc_auxv = args.auxv;
-
static void* tls[BIONIC_TLS_SLOTS];
-
static pthread_internal_t main_thread;
-
main_thread.tls = tls;
-
/*........*/
-
__set_tls(main_thread.tls);
-
tls[TLS_SLOT_BIONIC_PREINIT] = &args;
-
__init_alternate_signal_stack(&main_thread);
-
}
-
__LIBC_HIDDEN__ int __set_tls(void* ptr) {
-
struct user_desc tls_descriptor;
-
__init_user_desc(&tls_descriptor, true, ptr);
-
int rc = __set_thread_area(&tls_descriptor);
-
if (rc != -1) {
-
// Change %gs to be new GDT entry.
-
uint16_t table_indicator = 0; // GDT
-
uint16_t rpl = 3; // Requested privilege level
-
uint16_t selector = (tls_descriptor.entry_number << 3) | table_indicator | rpl;
-
__asm__ __volatile__("movw %w0, %%gs" : /*output*/ : "q"(selector) /*input*/ : /*clobber*/);
-
}
-
return rc;
-
}
__init_user_desc第二个参数为true,表示动态申请gdt中的位置,一般为6,所以gs=0x33。
后续在__set_thread_area函数里将指向tls[BIONIC_TLS_SLOTS]的地址写到了gdt[6]中。
数组前几项是固定的:
-
enum {
-
TLS_SLOT_SELF = 0, // The kernel requires this specific slot for x86.
-
TLS_SLOT_THREAD_ID,
-
TLS_SLOT_ERRNO,
-
// These two aren't used by bionic itself, but allow the graphics code to
-
// access TLS directly rather than using the pthread API.
-
TLS_SLOT_OPENGL_API = 3,
-
TLS_SLOT_OPENGL = 4,
-
TLS_SLOT_BIONIC_PREINIT = TLS_SLOT_OPENGL_API,
-
TLS_SLOT_STACK_GUARD = 5, // GCC requires this specific slot for x86.
-
TLS_SLOT_DLERROR,
-
TLS_SLOT_FIRST_USER_SLOT // Must come last!
-
};
bionic中的TLS表相当于一个一维数组。
bionic中不支持__thread语法,pthread_key_create, pthread_setspecific和pthread_getspecific三个函数直接折腾TLS_SLOT_FIRST_USER_SLOT之后的位置,目前TLS个数限制为64个。
以pthread_getspecific为例,看看bionic中的实现,比glibc简单多了:
-
void* pthread_getspecific(pthread_key_t key) {
-
if (!IsValidUserKey(key)) {
-
return NULL;
-
}
-
// For performance reasons, we do not lock/unlock the global TLS map
最后
感谢您的阅读,在文末给大家准备一个福利。本人从事Android开发已经有十余年,算是一名资深的移动开发架构师了吧。根据我的观察发现,对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。
所以在此将我十年载,从萌新小白一步步成长为Android移动开发架构师的学习笔记,从Android四大组件到手写实现一个架构设计,我都有一一的对应笔记为你讲解。
当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。
最后,赠与大家一句诗,共勉!
不驰于空想,不骛于虚声。不忘初心,方得始终。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
bionic中不支持__thread语法,pthread_key_create, pthread_setspecific和pthread_getspecific三个函数直接折腾TLS_SLOT_FIRST_USER_SLOT之后的位置,目前TLS个数限制为64个。
以pthread_getspecific为例,看看bionic中的实现,比glibc简单多了:
-
void* pthread_getspecific(pthread_key_t key) {
-
if (!IsValidUserKey(key)) {
-
return NULL;
-
}
-
// For performance reasons, we do not lock/unlock the global TLS map
最后
感谢您的阅读,在文末给大家准备一个福利。本人从事Android开发已经有十余年,算是一名资深的移动开发架构师了吧。根据我的观察发现,对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。
所以在此将我十年载,从萌新小白一步步成长为Android移动开发架构师的学习笔记,从Android四大组件到手写实现一个架构设计,我都有一一的对应笔记为你讲解。
当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。
[外链图片转存中…(img-jCdejvCj-1715850210939)]
最后,赠与大家一句诗,共勉!
不驰于空想,不骛于虚声。不忘初心,方得始终。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!