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

比如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. if (copy_from_user(&info, u_info, sizeof(info)))

  7. return -EFAULT;

  8. if (!tls_desc_okay(&info))

  9. return -EINVAL;

  10. if (idx == -1)

  11. idx = info.entry_number;

  12. /*

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

  14. * allocate an empty descriptor:

  15. */

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

  17. idx = get_free_idx();

  18. if (idx < 0)

  19. return idx;

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

  21. return -EFAULT;

  22. }

  23. if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)

  24. return -EINVAL;

  25. set_tls_desc(p, idx, &info, 1);

  26. return 0;

  27. }

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. /* We allocate one block of references here. This should be enough

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

  7. struct pthread_key_data

  8. {

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

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

  11. wrapping, too. */

  12. uintptr_t seq;

  13. /* Data pointer. */

  14. void *data;

  15. } specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];

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

  17. struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];

  18. /*......*/

  19. }

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. /* Special case access to the first 2nd-level block. This is the

  7. usual case. */

  8. if (__glibc_likely (key < PTHREAD_KEY_2NDLEVEL_SIZE))

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

  10. else

  11. {

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

  13. if (key >= PTHREAD_KEYS_MAX)

  14. /* Not valid. */

  15. return NULL;

  16. unsigned int idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE;

  17. unsigned int idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE;

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

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

  20. return NULL, too. */

  21. struct pthread_key_data *level2 = THREAD_GETMEM_NC (THREAD_SELF,

  22. specific, idx1st);

  23. if (level2 == NULL)

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

  25. return NULL;

  26. /* There is data. */

  27. data = &level2[idx2nd];

  28. }

  29. void *result = data->data;

  30. if (result != NULL)

  31. {

  32. uintptr_t seq = data->seq;

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

  34. result = data->data = NULL;

  35. }

  36. return result;

  37. }

三、bionic中的TLS

===========================================================================

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

  1. void __libc_init_tls(KernelArgumentBlock& args) {

  2. __libc_auxv = args.auxv;

  3. static void* tls[BIONIC_TLS_SLOTS];

  4. static pthread_internal_t main_thread;

  5. main_thread.tls = tls;

  6. /*........*/

  7. __set_tls(main_thread.tls);

  8. tls[TLS_SLOT_BIONIC_PREINIT] = &args;

  9. __init_alternate_signal_stack(&main_thread);

  10. }

  11. __LIBC_HIDDEN__ int __set_tls(void* ptr) {

  12. struct user_desc tls_descriptor;

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

  14. int rc = __set_thread_area(&tls_descriptor);

  15. if (rc != -1) {

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

  17. uint16_t table_indicator = 0; // GDT

  18. uint16_t rpl = 3; // Requested privilege level

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

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

  21. }

  22. return rc;

  23. }

__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. // 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简单多了:

  1. void* pthread_getspecific(pthread_key_t key) {

  2. if (!IsValidUserKey(key)) {

  3. return NULL;

  4. }

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

最后

感谢您的阅读,在文末给大家准备一个福利。本人从事Android开发已经有十余年,算是一名资深的移动开发架构师了吧。根据我的观察发现,对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

所以在此将我十年载,从萌新小白一步步成长为Android移动开发架构师的学习笔记,从Android四大组件到手写实现一个架构设计,我都有一一的对应笔记为你讲解。

当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。

[外链图片转存中…(img-1lpR0bLT-1715722877927)]

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 28
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
114啦网址导航建站系统历史版本功能特性: 源码发布地址:http://chouxiaozi001.qupan.cc/documents ====================================================================== V1.13 (2010年07月02日修复列表) .# 开启模板引擎安全选项,禁止<{php}>标签,消除自定义模板允许输入php代码的安全隐患; .# 修复站点提交模块的跨站脚本漏洞; V1.13 (2010年04月28日修复列表) .# 修复获取管理权限后的php执行代码漏洞; V1.13 (2009年12月14日修复列表) .# 对在线升级功能进行了改进,提高兼容性和稳定性; .# 对程序进行优化,后台运行效率明显加快; .# 修改地方服务子分类不可修改Bug; .# 修复修改网址信息后自动跳转会出现2000秒倒数的Bug; .# 修复天气自动判断显示为“东莞”的Bug; .# 修复内页不能正常显示用户自定义背景的Bug; .# 修复名站切换栏“我的收藏”网址IE浏览器下不在新窗口打开的Bug; .# 修复部分环境下后台左侧菜单不显示的Bug; .# 改进批量导入网址功能; .# 修正清空关键词分类后静态生成出错的Bug; .# 修正生成分类为空分类出错的Bug; V1.13 (2009年11月30日发布) .#增加在线升级功能,自动判断升级,方便及修复bug和后续版本升级; .#对程序进行优化,增强程序兼容性,运行效率更高; .#增加批量导入网址的功能,可在短间内导入更多数据; .#重写模版,简化模版标签,使用户制作模版更简单; .#增加模版选择器模块,规范模版制作,方便站长用户使用多种模版; .#优化首页搜索框调用和配置,使用户可以快速填入搜索联盟代码; .#修改广告调用模式,用户可直接写入代码,增强可操控性; .#新增站长需要的闹钟、滚动新闻、天气预报定制等功能; .#调整和优化系统菜单的功能及相关说明,使站长更方便使用; .#优化首页模版设计,与官方同步,经测试首页搜索框设计可增加搜索量; .#改进网站管理,分类管理的用户体验,操作更方便快捷; .#实现酷站导航分类独立化,改内页关联为独立设置,增强了可操作性; .#实现“行业网站”模块化,可独立定制行业网站; .#实现“地方服务”模块独立化,分列各省地方服务,丰富网站内容; .#实现首页分类导航和酷站导航自适应拉伸,减轻站长调试负担; .#修复V1.12版本站长们提交的各种BUG或者修改建议,不一一列出; V1.12 0921升级补丁包 (2009年9月22日发布) *修复站点修复功能SMTP邮箱个别情况下开启失败的问题 *修复天气预报接口失效的问题 *对列车刻查询插件性能优化,提高了查询速度 V1.12 0824升级补丁包 (2009年8月24日发布) *增加“天气详情”关联id的功能 *修复内页因为关键词词语联想不能使用的问题 *修复使用cnzz站长统计代码首页脚本错误的问题 *解决个别linux环境下,网址导航首页无法生成的问题 *修复删除所有数据后“查看所有分类”不能显示的问题 V1.12 升级补丁包 (2009年7月26日发布) *优化静态页生成功能,提高生成速度,解决php5环境下生成白屏的问题 *修改了服务器根目录不可写导致无法生成首页的问题 *修复分类生成错误及分类多级管理错误的问题 *修复后台点酷站管理排序及列表宽度不能自适应的问题 *修复内页title-站名合理位置 *修复顶部广告位图片高度和宽度的错误 *增加首页、内页、专题页面统计代码,修复统计代码引起的脚本错误 *修复"网址列表"删除选项打勾出现的“input”未定义的错误 *修复网站提交页面头部调用错误的问题 *修复后台安全日志显示问题 V1.12 (2009年7月23日发布) * 以最新架构对程序进行重写,并优化了后台管理,增强了用户体验和交互性; * 升级数据统筹管理中数据库备份、恢复等功能,V1.10及V1.11数据可导入升级; * 提供“红河MySITES网址导航”数据库转换插件,以便让更多人使用114啦建站系统; * 升级静态页面生成系统,可设置即更新、自定义分类更新、更新全部分类三种选择; * 修复了安装系统管理员账号不可自定义的功能; * 增加静态页页可在独立文件夹存放功能,如/html/youxi/index. htm; * 增加子分类页面KeyWords和Description自定义修改功能; * 增加模板后台管理功能,并对主要的模板和标签都进行注解说明; * 升级顶部广告位管理,完全支持html,方便联盟广告添加; * 调整“网站收录”的功能,对一般站点不需要的提交选项进行简化; * 优化回收站的管理和操作流程,使恢复和删除站点更方便; * 优化站点统计引擎和缓存引擎,使系统在保证灵活性的同也有更好的性能 * 优化网站管理细节,网站添加后台同分类里如有相同网址,将有友好的错误提示; * 增加自定义网址收藏夹功能,用户可自行收藏全站网址到首页,并可增删; * 增加一键清空原有114啦数据功能,方便站长做其它行业网址导航的需要; * 增加首页底部专题管理功能,如“地方服务”,“网络游戏”,后台可修改,增删,排序等 * 增加各级子分类批量删除功能; * 增加名站导航切换栏,可以在后台修改调用网址,如“常用软件”,“热门游戏”之类; * 增加天气预报的定制功能,并移至天气处可展开未来三天的天气预报; * 增加程序升级提醒,方便站长获得官方升级信息 * 增加宽屏版及其他个性设置,如字体颜色、页面背景、酷站对齐方式等设置; V1.11 (2009年6月30日发布) * 实现一键傻瓜式安装,无需手工导入数据库,轻松建站 * 整理了模版文件,使模版目录更清晰,制作风格更方便 * 修复了114啦数据库导入错误的问题 * 修复了统计代码不生效的问题 * 修复了生成静态页乱码的问题 * 修复了首页468x60 Banner广告设置不可用html代码的问题 * 解决了浏览记录个别情况下,使用不正常的问题 * 解决了“用户反馈”个别情况下不能使用的问题 V1.10 (2009年5月26日发布) * 采用Php+Mysql架构,搭建简易,程序运行稳定安全 * 集成114啦网址导航最新数据库,收录站点质量高 * 全站可见链接99%后台控制,站长可随心所欲定制 * 全站采用XHTML+CSS设计,符合W3C标准 * 一键全站html静态页生成,可完全脱离数据库运行 * 功能强大的多用户权限管理,可委任多个权限组进行站点维护 * 分类清晰人性化,可自定义分类链接、可外链,更符合seo标准 * 全站链接可设置上线间、过期间、颜色定制、留空、网站备注等,方便广告需要 * 提供多种风格的皮肤、天气预报、日历等个性化功能 * 集成115聚合搜索、百度、Google等引擎入口,全方位搜索体验 * 提供站内网址搜索、网站浏览记录等功能 * 系统安全设置:安全验证、系统负载控制、CC防护设置、IP禁止……将风险防患于未然 * 整合网站收录申请查询、用户反馈等程序,便于了解网友需求,有助于改善用户体验 * 完善的数据库管理,可进行在线数据库备份、恢复、优化及修复 * 全面的日志管理:后台管理安全日志、MYSQL、PHP错误日志,便于程序监控和操作记录 源码发布地址:http://chouxiaozi001.qupan.cc/documents

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值