浅析glibc中thread tls的一处bug

最早的时候是在程序初始化过程中开启了一个timer(timer_create),这个timer第一次触发的时间较短时就会引起程序core掉,core的位置也是不定的。使用valgrind可以发现有错误的内存写入:

==31676== Invalid write of size 8
==31676==    at 0x37A540F852: _dl_allocate_tls_init (in /lib64/ld-2.5.so)
==31676==    by 0x4E26BD3: [email protected]@GLIBC_2.2.5 (in /lib64/libpthread-2.5.so)
==31676==    by 0x76E0B00: timer_helper_thread (in /lib64/librt-2.5.so)
==31676==    by 0x4E2673C: start_thread (in /lib64/libpthread-2.5.so)
==31676==    by 0x58974BC: clone (in /lib64/libc-2.5.so)
==31676==  Address 0xf84dbd0 is 0 bytes after a block of size 336 alloc'd
==31676==    at 0x4A05430: calloc (vg_replace_malloc.c:418)
==31676==    by 0x37A5410082: _dl_allocate_tls (in /lib64/ld-2.5.so)
==31676==    by 0x4E26EB8: [email protected]@GLIBC_2.2.5 (in /lib64/libpthread-2.5.so)
==31676==    by 0x76E0B00: timer_helper_thread (in /lib64/librt-2.5.so)
==31676==    by 0x4E2673C: start_thread (in /lib64/libpthread-2.5.so)
==31676==    by 0x58974BC: clone (in /lib64/libc-2.5.so)

google _dl_allocate_tls_init 相关发现一个glibc的bug Bug 13862 和我的情况有点类似。本文就此bug及tls相关实现做一定阐述。

需要查看glibc的源码,如何确认使用的glibc的版本,可以这样:

$ /lib/libc.so.6
GNU C Library stable release version 2.5, by Roland McGrath et al.
...

为了方便,还可以直接在(glibc Cross Reference)[http://osxr.org/glibc/source/?v=glibc-2.17]网页上进行查看,版本不同,但影响不大。

BUG描述

要重现13862 BUG作者提到要满足以下条件:

The use of a relatively large number of dynamic libraries, loaded at runtime using dlopen.

The use of thread-local-storage within those libraries.

A thread exiting prior to the number of loaded libraries increasing a significant amount, followed by a new thread being created after the number of libraries has increased.

简单来说,就是在加载一大堆包含TLS变量的动态库的过程中,开启了一个线程,这个线程退出后又开启了另一个线程。

这和我们的问题场景很相似。不同的是我们使用的是timer,但timer在触发时也是开启新的线程,并且这个线程会立刻退出:

/nptl/sysdeps/unix/sysv/linux/timer_routines.c

timer_helper_thread(...)  // 用于检测定时器触发的辅助线程
{
    ...
      pthread_t th;
      (void) pthread_create (&th, &tk->attr, timer_sigev_thread, // 开启一个新线程调用用户注册的定时器函数
                 td);
    ...
}

要重现此BUG可以使用我的实验代码 thread-tls,或者使用Bug 13862 中的附件

TLS相关实现

可以顺着_dl_allocate_tls_init函数的实现查看相关联的部分代码。该函数遍历所有加载的包含TLS变量的模块,初始化一个线程的TLS数据结构。

每一个线程都有自己的堆栈空间,其中单独存储了各个模块的TLS变量,从而实现TLS变量在每一个线程中都有单独的拷贝。TLS与线程的关联关系可以查看下图:

应用层使用的pthread_t实际是个pthread对象的地址。创建线程时线程的堆栈空间和pthread结构是一块连续的内存。但这个地址并不指向这块内存的首地址。相关代码:/nptl/allocatestack.c
allocate_stack,该函数分配线程的堆栈内存。

pthread第一个成员是tcbhead_ttcbhead_tdtv指向了一个dtv_t数组,该数组的大小随着当前程序载入的模块多少而动态变化。每一个模块被载入时,都有一个l_tls_modid,其直接作为dtv_t数组的下标索引。tcbhead_t中的dtv实际指向的是dtv_t第二个元素,第一个元素用于记录整个dtv_t数组有多少元素,第二个元素也做特殊使用,从第三个元素开始,才是用于存储TLS变量。

一个dtv_t存储的是一个模块中所有TLS变量的地址,当然这些TLS变量都会被放在连续的内存空间里。dtv_t::pointer::val正是用于指向这块内存的指针。对于非动态加载的模块它指向的是线程堆栈的位置;否则指向动态分配的内存位置。

以上结构用代码描述为,

union dtv_t {
    size_t counter;
    struct {
        void *val; /* point to tls variable memory */
        bool is_static;
    } pointer;
};

struct tcbhead_t {
    void *tcb;
    dtv_t *dtv; /* point to a dtv_t array */
    void *padding[22]; /* other members i don't care */
};

struct pthread {
    tcbhead_t tcb;
    /* more members i don't care */
};

dtv是一个用于以模块为单位存储TLS变量的数组

实际代码参看 /nptl/descr.h 及 nptl/sysdeps/x86_64/tls.h。

实验

使用g++ -o thread -g -Wall -lpthread -ldl thread.cpp编译代码,即在创建线程前加载了一个.so:

Breakpoint 1, dump_pthread (id=1084229952) at thread.cpp:40
40          printf("pthread %p, dtv %p\n", pd, dtv);
(gdb) set $dtv=pd->tcb.dtv
(gdb) p $dtv[-1]
$1 = {counter = 17, pointer = {val = 0x11, is_static = false}}
(gdb) p $dtv[3]
$2 = {counter = 18446744073709551615, pointer = {val = 0xffffffffffffffff, is_static = false}}

dtv[3]对应着动态加载的模块,is_static=falseval被初始化为-1:

/elf/dl-tls.c _dl_allocate_tls_init

if (map->l_tls_offset == NO_TLS_OFFSET
   || map->l_tls_offset == FORCED_DYNAMIC_TLS_OFFSET)
 {
   /* For dynamically loaded modules we simply store
      the value indicating deferred allocation.  */
   dtv[map->l_tls_modid].pointer.val = TLS_DTV_UNALLOCATED;
   dtv[map->l_tls_modid].pointer.is_static = false;
   continue;
 }

dtv数组大小之所以为17,可以参看代码 /elf/dl-tls.c allocate_dtv

// dl_tls_max_dtv_idx 随着载入模块的增加而增加,载入1个.so则是1 

dtv_length = GL(dl_tls_max_dtv_idx) + DTV_SURPLUS; // DTV_SURPLUS 14
dtv = calloc (dtv_length + 2, sizeof (dtv_t));
if (dtv != NULL)
 {
   /* This is the initial length of the dtv.  */
   dtv[0].counter = dtv_length;

继续上面的实验,当调用到.so中的function时,其TLS被初始化,此时dtv[3]val指向初始化后的TLS变量地址:

68          fn();
(gdb)
0x601808, 0x601804, 0x601800
72          return 0;
(gdb) p $dtv[3]
$3 = {counter = 6297600, pointer = {val = 0x601800, is_static = false}}
(gdb) x/3xw 0x601800
0x601800:       0x55667788      0xaabbccdd      0x11223344

这个时候还可以看看dtv[1]中的内容,正是指向了pthread前面的内存位置:

(gdb) p $dtv[1]
$5 = {counter = 1084229936, pointer = {val = 0x40a00930, is_static = true}}
(gdb) p/x tid
$7 = 0x40a00940

结论:

  • 线程中TLS变量的存储是以模块为单位的

so模块加载

这里也并不太需要查看dlopen等具体实现,由于使用__thread来定义TLS变量,整个实现涉及到ELF加载器的一些细节,深入下去内容较多。这里直接通过实验的手段来了解一些实现即可。

上文已经看到,在创建线程前如果动态加载了.so,dtv数组的大小是会随之增加的。如果是在线程创建后再载入.so呢?

使用g++ -o thread -g -Wall -lpthread -ldl thread.cpp -DTEST_DTV_EXPAND -DSO_CNT=1编译程序,调试得到:

73          load_sos();
(gdb)
0x601e78, 0x601e74, 0x601e70

Breakpoint 1, dump_pthread (id=1084229952) at thread.cpp:44
44          printf("pthread %p, dtv %p\n", pd, dtv);
(gdb) p $dtv[-1]
$3 = {counter = 17, pointer = {val = 0x11, is_static = false}}
(gdb) p $dtv[4]
$4 = {counter = 6299248, pointer = {val = 0x601e70, is_static = false}}

在新载入了.so时,dtv数组大小并没有新增,dtv[4]直接被拿来使用。

因为dtv初始大小为16,那么当载入的.so超过这个数字的时候会怎样?

使用g++ -o thread -g -Wall -lpthread -ldl thread.cpp -DTEST_DTV_EXPAND编译程序:

...
pthread 0x40a00940, dtv 0x6016a0
...
Breakpoint 1, dump_pthread (id=1084229952) at thread.cpp:44
44          printf("pthread %p, dtv %p\n", pd, dtv);
(gdb) p dtv
$2 = (dtv_t *) 0x6078a0
(gdb) p dtv[-1]
$3 = {counter = 32, pointer = {val = 0x20, is_static = false}}
(gdb) p dtv[5]
$4 = {counter = 6300896, pointer = {val = 0x6024e0, is_static = false}}

可以看出,dtv被重新分配了内存(0x6016a0 -> 0x6078a0)并做了扩大。

以上得出结论:

  • 创建线程前dtv的大小会根据载入模块数量决定
  • 创建线程后新载入的模块会动态扩展dtv的大小(必要的时候)

pthread堆栈重用

allocate_stack中分配线程堆栈时,有一个从缓存中取的操作:

allocate_stack(..) {
    ...
    pd = get_cached_stack (&size, &mem);
    ...
}
/* Get a stack frame from the cache.  We have to match by size since
   some blocks might be too small or far too large.  */
get_cached_stack(...) {
    ...
    list_for_each (entry, &stack_cache) // 根据size从stack_cache中取
    { ... }
    ...
    /* Clear the DTV.  */
    dtv_t *dtv = GET_DTV (TLS_TPADJ (result));
    for (size_t cnt = 0; cnt < dtv[-1].counter; ++cnt)
        if (! dtv[1 + cnt].pointer.is_static
                && dtv[1 + cnt].pointer.val != TLS_DTV_UNALLOCATED)
            free (dtv[1 + cnt].pointer.val);
    memset (dtv, '\0', (dtv[-1].counter + 1) * sizeof (dtv_t));

    /* Re-initialize the TLS.  */
    _dl_allocate_tls_init (TLS_TPADJ (result));
}

get_cached_stack会把取出的pthread中的dtv重新初始化。注意
_dl_allocate_tls_init
中是根据模块列表来初始化dtv数组的。

实验

当一个线程退出后,它就可能被当做cache被get_cached_stack取出复用。

使用g++ -o thread -g -Wall -lpthread -ldl thread.cpp -DTEST_CACHE_STACK编译程序,运行:

$ ./thread
..
pthread 0x413c9940, dtv 0x1be46a0
...
pthread 0x413c9940, dtv 0x1be46a0

回顾BUG

当新创建的线程复用了之前退出的线程堆栈时,由于在_dl_allocate_tls_init中初始化dtv数组时是根据当前载入的模块数量而定。如果在这个时候模块数已经超过了这个复用的dtv数组大小,那么就会出现写入非法的内存。使用valgrind检测就会得到本文开头提到的结果。

由于dtv数组大小通常会稍微大点,所以在新加载的模块数量不够多时程序还不会有问题。可以通过控制测试程序中SO_CNT的大小看看dtv中内容的变化。

另外,我查看了下glibc的更新历史,到目前为止(2.20)这个BUG还没有修复。

参考文档

原文地址: http://codemacro.com/2014/10/07/pthread-tls-bug/

written by Kevin Lynx  posted at
http://codemacro.com

时间: 2024-10-25 03:19:18

浅析glibc中thread tls的一处bug的相关文章

浅析 Linux 中的时间编程和实现原理一—— Linux 应用层的时间编程【转】

本文转载自:http://www.cnblogs.com/qingchen1984/p/7007631.html 本篇文章主要介绍了"浅析 Linux 中的时间编程和实现原理一—— Linux 应用层的时间编程",主要涉及到浅析 Linux 中的时间编程和实现原理一—— Linux 应用层的时间编程方面的内容,对于浅析 Linux 中的时间编程和实现原理一—— Linux 应用层的时间编程感兴趣的同学可以参考一下. 简介: 本文试图完整地描述 Linux 系统中 C 语言编程中的时间问

浅析JDK中ServiceLoader的源码

前提 紧接着上一篇<通过源码浅析JDK中的资源加载>,ServiceLoader是SPI(Service Provider Interface)中的服务类加载的核心类,也就是,这篇文章先介绍ServiceLoader的使用方式,再分析它的源码. ServiceLoader的使用 这里先列举一个经典的例子,MySQL的Java驱动就是通过ServiceLoader加载的,先引入mysql-connector-java的依赖: <dependency> <groupId>m

iOS7上在xib中使用UITableViewController设置背景色bug

今天用xcode5.1设置xib中,用静态的方式设置UITableViewController中的tableview,把tableview中的backgroundColor改变后,xib上有效果,但是一运行就变成了透明色,在过渡动画时,都可以看到背面的view!见下面截图 后来在viewdidload中设置一下就好了 self.tableView.backgroundColor = [UIColor blackColor]; 我感觉这是xcode的一个bug! 而且这种static的设置方式,如

浅析python中_name_=&#39;_main_&#39;

刚接触到python时,对代码中的_name_='_main_'比较疑惑,本文对其的讲解借鉴了其他博客讲述(见参考资料),希望和大家共同学习. Make a script both importable and executable 首先先看一个例子 1 #module.py 2 def main(): 3 print "we are in %s"%__name__ 4 if __name__ == '__main__': 5 main() 在这段函数中,定义main函数,当py文件被

浅析python 中__name__ = &#39;__main__&#39; 的作用

很多新手刚开始学习python的时候经常会看到python 中__name__ = \'__main__\' 这样的代码,可能很多新手一开始学习的时候都比较疑惑,python 中__name__ = '__main__' 的作用,到底干嘛的? 有句话经典的概括了这段代码的意义: "Make a script both importable and executable" 意思就是说让你写的脚本模块既可以导入到别的模块中用,另外该模块自己也可执行. __name__ 是当前模块名,当模块

从源码中浅析Android中如何利用attrs和styles定义控件

一直有个问题就是,Android中是如何通过布局文件,就能实现控件效果的不同呢?比如在布局文件中,我设置了一个TextView,给它设置了textColor,它就能够改变这个TextView的文本的颜色.这是如何做到的呢?我们分3个部分来看这个问题1.attrs.xml  2.styles.xml  3.看组件的源码. 1.attrs.xml: 我们知道Android的源码中有attrs.xml这个文件,这个文件实际上定义了所有的控件的属性,就是我们在布局文件中设置的各类属性 你可以找到attr

[js]uploadify结合jqueryUI弹出框上传,js中的冒出的bug,又被ie坑了

引言 最近在一个项目中,在用户列表中需要对给没有签名样本的个别用户上传签名的样本,就想到博客园中上传图片使用弹出框方式,博客园具体怎么实现的不知道,只是如果自己来弄,想到两个插件的结合使用,在弹出框中使用uploadify插件进行上传,每次都会报错很是无语,最后找到解决方案,这里记录一下,算是对工作中遇到的bug的一个总结. bug 这是vs调试状态下,显示的信息.在浏览器端,点击第一次上传按钮,正常,将弹出框关闭后,第二次打开,就会出现问题: 第一次单击上传: 第二次,关闭弹出框,再次单击上传

ShardedJedisPool 中可用连接数的小bug

ShardedJedisPool中,returnBrokenResource() 及 returnResource() ,为施放资源.关闭连接的方法,若重复调用,导致 _numActive 当前活动数一直递减,会出现负数的情况. 假如在一个方法中设置了三个jedis连接,在获取第一或第二个连接时出现异常,在抛出异常或者finally中总是施放这三个资源,会导致池中的连接连续施放三次,从而变成负数. 这样会出现连接池最大连接数配置无效的情况. 以下片段代码: public class RedisU

Delphi中取整函数Round的Bug解决

Delphi中 Round函数有个Bug一旦参数是形如 XXX.5这样的数时如果 XXX 是奇数 那么就会 Round up如果 XXX 是偶数 那么就会 Round down例如 Round(17.5)=18但是 Round(12.5)=12下面的函数即可纠正这个 Bug 但是是临时性的执行 DoRound(12.5) 结果为 13 正确 [delphi] view plain copy function DoRound(Value: Extended): Int64; procedure S