使用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调试验证可以看: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;
}

 


三、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的机制。

 

参考:

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

Linux内核设计的艺术

浅析glibc中thread tls的一处bug:http://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的驱动:http://blog.csdn.net/jinzhuojun/article/details/41412587

 

目录
相关文章
|
3月前
|
存储 算法 Linux
在Linux中,如何理解加密工具?如GnuPG和OpenSSL。
在Linux中,如何理解加密工具?如GnuPG和OpenSSL。
|
2月前
|
Linux C语言 容器
在低版本glibc的环境里运行基于高版本glibc编译的可执行程序
在低版本glibc的环境里运行基于高版本glibc编译的可执行程序
|
5月前
|
存储 前端开发 区块链
常见的 EVM 版本以及它们的区别
常见的 EVM 版本以及它们的区别
88 5
|
5月前
|
Ubuntu Linux 编译器
当自身需要使用的 gcc版本 和Linux 默认版本 存在大版本差异时怎样处理
当自身需要使用的 gcc版本 和Linux 默认版本 存在大版本差异时怎样处理
120 2
|
6月前
|
Linux 编译器 C语言
解决 glibc 版本冲突:实用指南与策略分析
解决 glibc 版本冲突:实用指南与策略分析
809 2
|
消息中间件 Linux 网络安全
让终端支持https,移植OpenSSL和libcurl到嵌入式linux,遇到的问题总结
让终端支持https,移植OpenSSL和libcurl到嵌入式linux,遇到的问题总结
|
算法 安全 开发工具
BabaSSL 与其他 openssl 版本库共存方案
背景随着《密码法》颁布和实施以来,商密算法(SM)和国密握手协议(TLCP)越来越重要,各银行、金融和安全企业纷纷对自己的软件进行改造,以应对监管的检查,一些大型软件或者客户端依赖了很多第三方库,第三方库可能依赖了 openssl,并以静态库的方式打包到一个 SDK 中,然后再提供给客户端软件使用,这时候引入 BabaSSL 进行国密改造的话,会面临 BabaSSL 与其他 openssl 同时依
1503 0
BabaSSL 与其他 openssl 版本库共存方案
|
Linux C语言
arm开发板更新gcc/gcc++|Deb bain 更新 gcc,无需编译直接更新gcc三
arm开发板更新gcc/gcc++|Deb bain 更新 gcc,无需编译直接更新gcc三
434 0
arm开发板更新gcc/gcc++|Deb bain 更新 gcc,无需编译直接更新gcc三
|
Ubuntu 开发工具 C语言
clickhouse如何解决GLIBC不兼容问题
clickhouse如何解决GLIBC不兼容问题
402 0
|
关系型数据库 应用服务中间件 PHP
Debian 下轻松实现 PHP 多版本共存
PHP7 的发布废弃了部分特性,一些程序或者插件当前不能很好的运行在 PHP7 上,因此如果服务器上能同时运行多个 PHP 的版本,就可以很好的解决过渡期的兼容性问题。在前面两篇文章中,我们介绍了如何在 Debian Stretch 中安装使用 PHP5,以及如何在Debian环境安装配置PHP 7.2。
2168 0