Libhybris之Glibc和Bionic共存时的TLS问题(四)

如无特殊说明,系统为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:

  1. __thread int x = 3;

  2. printf("%d\n", x);

  3.  
  4. pthread_key_t key;

  5. pthread_key_create(&key, NULL);

  6. pthread_setspecific(key,"hello world");

  7. 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描述符的内容:

  1. int do_set_thread_area(struct task_struct *p, int idx,

  2. struct user_desc __user *u_info,

  3. int can_allocate)

  4. {

  5. struct user_desc info;

  6.  
  7. if (copy_from_user(&info, u_info, sizeof(info)))

  8. return -EFAULT;

  9.  
  10. if (!tls_desc_okay(&info))

  11. return -EINVAL;

  12.  
  13. if (idx == -1)

  14. idx = info.entry_number;

  15.  
  16. /*

  17. * index -1 means the kernel should try to find and

  18. * allocate an empty descriptor:

  19. */

  20. if (idx == -1 && can_allocate) {

  21. idx = get_free_idx();

  22. if (idx < 0)

  23. return idx;

  24. if (put_user(idx, &u_info->entry_number))

  25. return -EFAULT;

  26. }

  27.  
  28. if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)

  29. return -EINVAL;

  30.  
  31. set_tls_desc(p, idx, &info, 1);

  32.  
  33. return 0;

  34. }

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,等等信息。

二、glibc中的TLS

设置gs的值,以及gdt[6]中的地址,是在如下代码中进行设置的:

  1. /* Code to initially initialize the thread pointer. This might need

  2. special attention since 'errno' is not yet available and if the

  3. operation can cause a failure 'errno' must not be touched. */

  4. # define TLS_INIT_TP(thrdescr) \

  5. ({ void *_thrdescr = (thrdescr); \

  6. tcbhead_t *_head = _thrdescr; \

  7. union user_desc_init _segdescr; \

  8. int _result; \

  9. \

  10. _head->tcb = _thrdescr; \

  11. /* For now the thread descriptor is at the same address. */ \

  12. _head->self = _thrdescr; \

  13. /* New syscall handling support. */ \

  14. INIT_SYSINFO; \

  15. \

  16. /* Let the kernel pick a value for the 'entry_number' field. */ \

  17. tls_fill_user_desc (&_segdescr, -1, _thrdescr); \

  18. \

  19. /* Install the TLS. */ \

  20. INTERNAL_SYSCALL_DECL (err); \

  21. _result = INTERNAL_SYSCALL (set_thread_area, err, 1, &_segdescr.desc); \

  22. /*........*/

tls_fill_user_desc第二个参数为-1,表示动态申请gdt中的位置,一般为6,所以gs=0x33。

在_segdescr中保存了tcbhead_t的地址,后续在set_thread_area系统调用里将tcbhead_t的地址写到了gdt[6]中。

 

  1. typedef struct

  2. {

  3. void *tcb; // 指向tcbhead_t自己

  4. dtv_t *dtv; // 指向dtv数据,用于__thread类型的TLS的实现

  5. void *self; // 指向struct pthread结构体

  6. int multiple_threads;

  7. uintptr_t sysinfo; // 快速系统调用时的入口

  8. uintptr_t stack_guard;

  9. uintptr_t pointer_guard;

  10. int gscope_flag;

  11. #ifndef __ASSUME_PRIVATE_FUTEX

  12. int private_futex;

  13. #else

  14. int __glibc_reserved1;

  15. #endif

  16. /* Reservation of some values for the TM ABI. */

  17. void *__private_tm[4];

  18. /* GCC split stack support. */

  19. void *__private_ss;

  20. } tcbhead_t;

如何去获得上述代码保存的tcbhead_t,以pthread_self的实现为例:

  1. # define THREAD_SELF \

  2. ({ struct pthread *__self; \

  3. asm ("movl %%gs:%c1,%0" : "=r" (__self) \

  4. : "i" (offsetof (struct pthread, header.self))); \

  5. __self;})


PS:x86时,pthread结构体的第一个元素就是tcbhead_t,所以他们的地址相同:

  1. struct pthread

  2. {

  3. union

  4. {

  5. #if !TLS_DTV_AT_TP

  6. /* This overlaps the TCB as used for TLS without threads (see tls.h). */

  7. tcbhead_t header;

  8. #else

  9. /*........*/

glibc中的TLS,可以分为两类,三种。

第一类通过dtv_t *dtv实现,这是一个数组,数组里面每一项都是dtv_t联合体。

  1. typedef union dtv

  2. {

  3. size_t counter;

  4. struct

  5. {

  6. void *val;

  7. bool is_static;

  8. } pointer;

  9. } 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中:

  1. struct pthread

  2. {

  3. tcbhead_t header;

  4.  
  5. /*......*/

  6.  
  7. /* We allocate one block of references here. This should be enough

  8. to avoid allocating any memory dynamically for most applications. */

  9. struct pthread_key_data

  10. {

  11. /* Sequence number. We use uintptr_t to not require padding on

  12. 32- and 64-bit machines. On 64-bit machines it helps to avoid

  13. wrapping, too. */

  14. uintptr_t seq;

  15.  
  16. /* Data pointer. */

  17. void *data;

  18. } specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];

  19.  
  20. /* Two-level array for the thread-specific data. */

  21. struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];

  22.  
  23. /*......*/

  24. }

specific是一个二维数组,specific_1stblock是第一个一维数组,用于加快访问速度的。

通过pthread_key_create, pthread_setspecific和pthread_getspecific三个函数来折腾。

以pthread_getspecific来看怎么找到specific(gs--->gdt6--->tcbhead_t--->self--->THREAD_SELF--->specific)和使用二维数组的,比较简单:

  1. void *

  2. __pthread_getspecific (key)

  3. pthread_key_t key;

  4. {

  5. struct pthread_key_data *data;

  6.  
  7. /* Special case access to the first 2nd-level block. This is the

  8. usual case. */

  9. if (__glibc_likely (key < PTHREAD_KEY_2NDLEVEL_SIZE))

  10. data = &THREAD_SELF->specific_1stblock[key];

  11. else

  12. {

  13. /* Verify the key is sane. */

  14. if (key >= PTHREAD_KEYS_MAX)

  15. /* Not valid. */

  16. return NULL;

  17.  
  18. unsigned int idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE;

  19. unsigned int idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE;

  20.  
  21. /* If the sequence number doesn't match or the key cannot be defined

  22. for this thread since the second level array is not allocated

  23. return NULL, too. */

  24. struct pthread_key_data *level2 = THREAD_GETMEM_NC (THREAD_SELF,

  25. specific, idx1st);

  26. if (level2 == NULL)

  27. /* Not allocated, therefore no data. */

  28. return NULL;

  29.  
  30. /* There is data. */

  31. data = &level2[idx2nd];

  32. }

  33.  
  34. void *result = data->data;

  35. if (result != NULL)

  36. {

  37. uintptr_t seq = data->seq;

  38.  
  39. if (__glibc_unlikely (seq != __pthread_keys[key].seq))

  40. result = data->data = NULL;

  41. }

  42.  
  43. return result;

  44. }

三、bionic中的TLS

设置gs的值,以及gdt[6]中的地址,是在如下代码中进行设置的:

  1. void __libc_init_tls(KernelArgumentBlock& args) {

  2. __libc_auxv = args.auxv;

  3.  
  4. static void* tls[BIONIC_TLS_SLOTS];

  5. static pthread_internal_t main_thread;

  6. main_thread.tls = tls;

  7.  
  8. /*........*/

  9.  
  10. __set_tls(main_thread.tls);

  11. tls[TLS_SLOT_BIONIC_PREINIT] = &args;

  12.  
  13. __init_alternate_signal_stack(&main_thread);

  14. }

  15. __LIBC_HIDDEN__ int __set_tls(void* ptr) {

  16. struct user_desc tls_descriptor;

  17. __init_user_desc(&tls_descriptor, true, ptr);

  18.  
  19. int rc = __set_thread_area(&tls_descriptor);

  20. if (rc != -1) {

  21. // Change %gs to be new GDT entry.

  22.  
  23. uint16_t table_indicator = 0; // GDT

  24.  
  25. uint16_t rpl = 3; // Requested privilege level

  26.  
  27. uint16_t selector = (tls_descriptor.entry_number << 3) | table_indicator | rpl;

  28. __asm__ __volatile__("movw %w0, %%gs" : /*output*/ : "q"(selector) /*input*/ : /*clobber*/);

  29. }

  30.  
  31. return rc;

  32. }

__init_user_desc第二个参数为true,表示动态申请gdt中的位置,一般为6,所以gs=0x33。

后续在__set_thread_area函数里将指向tls[BIONIC_TLS_SLOTS]的地址写到了gdt[6]中。

数组前几项是固定的:

  1. enum {

  2. TLS_SLOT_SELF = 0, // The kernel requires this specific slot for x86.

  3. TLS_SLOT_THREAD_ID,

  4. TLS_SLOT_ERRNO,

  5. // These two aren't used by bionic itself, but allow the graphics code to

  6. // access TLS directly rather than using the pthread API.

  7. TLS_SLOT_OPENGL_API = 3,

  8. TLS_SLOT_OPENGL = 4,

  9. TLS_SLOT_BIONIC_PREINIT = TLS_SLOT_OPENGL_API,

  10. TLS_SLOT_STACK_GUARD = 5, // GCC requires this specific slot for x86.

  11. TLS_SLOT_DLERROR,

  12. TLS_SLOT_FIRST_USER_SLOT // Must come last!

  13. };

bionic中的TLS表相当于一个一维数组。

bionic中不支持__thread语法,pthread_key_create, pthread_setspecific和pthread_getspecific三个函数直接折腾TLS_SLOT_FIRST_USER_SLOT之后的位置,目前TLS个数限制为64个。

以pthread_getspecific为例,看看bionic中的实现,比glibc简单多了:

  1. void* pthread_getspecific(pthread_key_t key) {

  2. if (!IsValidUserKey(key)) {

  3. return NULL;

  4. }

  5.  
  6. // For performance reasons, we do not lock/unlock the global TLS map

  7. // to check that the key is properly allocated. If the key was not

  8. // allocated, the value read from the TLS should always be NULL

  9. // due to pthread_key_delete() clearing the values for all threads.

  10. return __get_tls()[key];

  11. }

# define __get_tls() ({ void** __val; __asm__("movl %%gs:0, %0" : "=r"(__val)); __val; })

 

四、什么是libhybris

libhybris简而言之,就是glibc想使用bionic中的.so。但是pthread,ipc等很多东西又不兼容,所以就整了这么一套东西。
libhybris实现了类似于android bionic的linker, 加一些glue code和wrap,hook之类的东西,去处理不兼容的部分。
demo程序,android端提供一个libfoo.c,里面有foo和bar两个函数:
Android.mk:

  1. LOCAL_PATH:=$(call my-dir)

  2. include $(CLEAR_VARS)

  3. LOCAL_MODULE:= libfoo

  4. LOCAL_SRC_FILES:= foo.cpp

  5. include $(BUILD_SHARED_LIBRARY)

foo.cpp:

  1. #include

  2. #include

  3.  
  4. void foo(void)

  5. {

  6. printf("foo\n");

  7. printf("%s\n", getenv("PATH"));

  8. }

  9.  
  10. void bar(void)

  11. {

  12. foo();

  13. printf("bar\n");

  14. }

其他系统端,通过libhybris,调用bar函数:

  1. #include

  2. #include

  3. #include

  4. #include

  5. #include

  6.  
  7. int main(void)

  8. {

  9. void *handle;

  10. void (*bar)(void);

  11.  
  12. handle = android_dlopen("libfoo.so", RTLD_NOW);

  13. if (NULL == handle)

  14. {

  15. fprintf(stderr, "android_dlopen failed: %s\n", strerror(errno));

  16. return -1;

  17. }

  18.  
  19. bar = (void (*)(void))android_dlsym(handle, "_Z3barv");

  20. if (NULL == bar)

  21. {

  22. fprintf(stderr, "fail to dlsym: %s\n", strerror(errno));

  23. return -1;

  24. }

  25. bar();

  26.  
  27. return 0;

  28. }


比较有意思的是,如果你的libhybris的hooks.c中对getenv进行了hook,你将发现调用的是hook函数,而不是android端的getenv。
PS:android_dlsym的函数不会进行hook,如果它调用了其他的bionic函数,其他的bionic函数可以被hook为glibc中的实现。
 

五、用libhybris时有什么问题

glibc和bionic共存时,bionic TLS数组会覆盖glibc中的tcbhead_t结构体。
比如gs:12既是glibc的multiple_threads,又是bionic的TLS_SLOT_OPENGL_API。gs:16既是glibc的sysinfo,又是bionic的TLS_SLOT_OPENGL。


android的代码/device/generic/goldfish/opengl/system/OpenglSystemCommon/ThreadInfo.cpp:

 
  1. static void tlsDestruct(void *ptr)

  2. {

  3. if (ptr) {

  4. EGLThreadInfo *ti = (EGLThreadInfo *)ptr;

  5. delete ti->hostConn;

  6. delete ti;

  7. ((void **)__get_tls())[TLS_SLOT_OPENGL] = NULL;

  8. }

  9. }

设置了gs:16=0,那么glibc中的tcbhead_t->sysinfo就为0了。

sysinfo是快速系统调用的入口,用于代替旧的int 0x80方式的系统调用。如果sysinfo为0,那么linux系统调用就无法工作了。
 

其他位置覆盖也会有各种问题,一般来说TLS_SLOT_OPENGL_API和TLS_SLOT_OPENGL比较严重些。

 

下图说明了详细的内存布局和覆盖情况:

Libhybris之Glibc和Bionic共存时的TLS问题(四)_第1张图片


 

六、解决方案


首先值得一提的是libhybris中的hook功能,可以在运行时把bionic实现的函数,替换为glibc实现的函数。如果我们把bionic中所有的有冲突的函数都hook了,那么就不会有什么问题了。

目前libhybris对bionic的pthread_setspecific和pthread_getspecific已经有了hook了,所以bionic TLS_SLOT_FIRST_USER_SLOT之后的TLS都不会有什么冲突,剩下的问题就是前面6、7个,比较严重了也就是TLS_SLOT_OPENGL_API和TLS_SLOT_OPENGL两个位置。

1、在android中把TLS_SLOT_OPENGL_API和TLS_SLOT_OPENGL从3,4改为其他的数值,比如6,7。

Ubuntu touch就是这么干的,但是仅部分解决了tls覆盖的问题。

外来的bionic程序,如果使用修改前的工具链编译,而且使用了TLS_SLOT_OPENGL_API和TLS_SLOT_OPENGL,就会有问题。

2、在glibc中的tcbhead_t中预留几个位置,给bionic的前几个TLS。

如果是x86的话,预留的位置需要在void *tcb之后,因为TLS_TCB_AT_TP为1。虽然还有一个位置有覆盖,不过void *tcb的作用和TLS_SLOT_SELF一样,所以恰好没影响。

如果是arm/aarch64的话,还需要修改ld.glod ld.bfd中的TCB_SIZE/ARM_TCB_SIZE,否则executable的__thread变量位置不对,shared library不受影响。

外来的glibc程序,仅当arm/aarch64并且是executable时,__thread变量会有问题。

推荐使用这种方式。

3、在libhybris中hook bionic中所有的有冲突的函数。

android中使用TLS的有些代码是inline的,有些是内嵌汇编,没法直接进行hook,需要对代码进行一些修改,而且修改的比较多,很可能有遗漏。

外来的bionic程序,如果有使用前几个tls的函数没有被hook到,会有问题。

 

你可能感兴趣的:(Libhybris学习)