閱讀193 返回首頁    go 技術社區[雲棲]


使用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:

 

__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));

 

3、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中實現的,不全是在內核中的。

 

上述代碼中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=0x33,gdt6,地址指向的其實是tcbhead_t結構體:

 

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;

 

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調試驗證可以看:https://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;
}

 


三、bionic中的TLS

bionic中的TLS實現就比較簡單了。gs=0x33, gdt6,地址指向TLS數組。數組前幾項是固定的:

 

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
  // to check that the key is properly allocated. If the key was not
  // allocated, the value read from the TLS should always be NULL
  // due to pthread_key_delete() clearing the values for all threads.
  return __get_tls()[key];
}

# 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:

 

LOCAL_PATH:=$(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE:= libfoo

LOCAL_SRC_FILES:= foo.cpp

include $(BUILD_SHARED_LIBRARY)

foo.cpp:

 

#include <stdio.h>
#include <stdlib.h>

void foo(void)
{
    printf("foo\n");
    printf("%s\n", getenv("PATH"));
}

void bar(void)
{
    foo();
    printf("bar\n");
}

 

其他係統端,通過libhybris,調用bar函數:

 

 

#include <stdio.h>
#include <hybris/common/binding.h>
#include <string.h>
#include <errno.h>
#include <dlfcn.h>

int main(void)
{
    void *handle;
    void (*bar)(void);

    handle = android_dlopen("libfoo.so", RTLD_NOW);
    if (NULL == handle)
    {
        fprintf(stderr, "android_dlopen failed: %s\n", strerror(errno));
        return -1;
    }

    bar = (void (*)(void))android_dlsym(handle, "_Z3barv");
    if (NULL == bar)
    {
        fprintf(stderr, "fail to dlsym: %s\n", strerror(errno));
        return -1;
    }
    bar();

    return 0;
}

比較有意思的是,如果你的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:

 

static void tlsDestruct(void *ptr)
{
    if (ptr) {
        EGLThreadInfo *ti = (EGLThreadInfo *)ptr;
        delete ti->hostConn;
        delete ti;
        ((void **)__get_tls())[TLS_SLOT_OPENGL] = NULL;
    }
}

設置了gs:16=0,那麼glibc中的tcbhead_t->sysinfo就為0了。

 

sysinfo是快速係統調用的入口,用於代替舊的int 0x80方式的係統調用。如果sysinfo為0,那麼linux係統調用就無法工作了。

 

其他位置覆蓋也會有各種問題,一般來說TLS_SLOT_OPENGL_API和TLS_SLOT_OPENGL比較嚴重些。

 

六、解決方案

首先值得一提的是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就是這麼幹的。

 

2、在glibc中的tcbhead_t中預留一些空間,給bionic的前幾個TLS,應該是行得通的,但是沒有測試過。

 

3、在libhybris中hook bionic中所有的有衝突的函數,但是android中使用TLS的有些代碼是inline的,有些是內嵌匯編,沒法直接進行hook,需要對代碼進行一些修改。

 

1和3影響外來的android的東西,可能運行有問題;2影響外來的glibc的東西,可能運行有問題。如果所有的都是有源碼,用同樣的工具鏈編譯的,那麼就沒什麼問題了。

 

我使用的是方案3,利用libhybris中已有的一個hook函數__get_tls_hooks,在android bionic中實現一個__get_tls_hooks函數,C語言的符號,去掉inline,加上__attribute__((visibility("default")))確保能夠導出符號,能夠被libhybris hook上。

然後修改諸如getEGLThreadInfo,getEGLThreadInfo改為使用__get_tls_hooks來實現的。

最後設置USE_SLOW_BINDING為1,防止android中通過gs相關的匯編函數繞過hook的機製。

 

參考:

https://man7.org/linux/man-pages/man7/pthreads.7.html

Linux內核設計的藝術

淺析glibc中thread tls的一處bug:https://codemacro.com/2014/10/07/pthread-tls-bug/

https://android.googlesource.com/platform/bionic/+/ics-mr1-release/libc/docs/OVERVIEW.TXT

libhybris及EGL Platform-在Glibc生態中重用Android的驅動:https://blog.csdn.net/jinzhuojun/article/details/41412587

 

最後更新:2017-10-26 17:34:46

  上一篇:go  杭電建新型教學平台,引入阿裏雲雲效走進高校課堂
  下一篇:go  ubuntu12.04環境下使用kvm ioctl接口實現最簡單的虛擬機