青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

loop_in_codes

低調(diào)做技術(shù)__歡迎移步我的獨(dú)立博客 codemaro.com 微博 kevinlynx

淺析glibc中thread tls的一處bug

最早的時(shí)候是在程序初始化過程中開啟了一個(gè)timer(timer_create),這個(gè)timer第一次觸發(fā)的時(shí)間較短時(shí)就會(huì)引起程序core掉,core的位置也是不定的。使用valgrind可以發(fā)現(xiàn)有錯(cuò)誤的內(nèi)存寫入:

==31676== Invalid write of size 8
==31676==    at 0x37A540F852: _dl_allocate_tls_init (in /lib64/ld-2.5.so)
==31676==    by 0x4E26BD3: pthread_create@@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: pthread_create@@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 相關(guān)發(fā)現(xiàn)一個(gè)glibc的bug Bug 13862 和我的情況有點(diǎn)類似。本文就此bug及tls相關(guān)實(shí)現(xiàn)做一定闡述。

需要查看glibc的源碼,如何確認(rèn)使用的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]網(wǎng)頁上進(jìn)行查看,版本不同,但影響不大。

BUG描述

要重現(xiàn)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.

簡(jiǎn)單來說,就是在加載一大堆包含TLS變量的動(dòng)態(tài)庫的過程中,開啟了一個(gè)線程,這個(gè)線程退出后又開啟了另一個(gè)線程。

這和我們的問題場(chǎng)景很相似。不同的是我們使用的是timer,但timer在觸發(fā)時(shí)也是開啟新的線程,并且這個(gè)線程會(huì)立刻退出:

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

timer_helper_thread(...)  // 用于檢測(cè)定時(shí)器觸發(fā)的輔助線程
{
    ...
      pthread_t th;
      (void) pthread_create (&th, &tk->attr, timer_sigev_thread, // 開啟一個(gè)新線程調(diào)用用戶注冊(cè)的定時(shí)器函數(shù)
                 td);
    ...
}

要重現(xiàn)此BUG可以使用我的實(shí)驗(yàn)代碼 thread-tls,或者使用Bug 13862 中的附件

TLS相關(guān)實(shí)現(xiàn)

可以順著_dl_allocate_tls_init函數(shù)的實(shí)現(xiàn)查看相關(guān)聯(lián)的部分代碼。該函數(shù)遍歷所有加載的包含TLS變量的模塊,初始化一個(gè)線程的TLS數(shù)據(jù)結(jié)構(gòu)。

每一個(gè)線程都有自己的堆棧空間,其中單獨(dú)存儲(chǔ)了各個(gè)模塊的TLS變量,從而實(shí)現(xiàn)TLS變量在每一個(gè)線程中都有單獨(dú)的拷貝。TLS與線程的關(guān)聯(lián)關(guān)系可以查看下圖:

應(yīng)用層使用的pthread_t實(shí)際是個(gè)pthread對(duì)象的地址。創(chuàng)建線程時(shí)線程的堆棧空間和pthread結(jié)構(gòu)是一塊連續(xù)的內(nèi)存。但這個(gè)地址并不指向這塊內(nèi)存的首地址。相關(guān)代碼:/nptl/allocatestack.c allocate_stack,該函數(shù)分配線程的堆棧內(nèi)存。

pthread第一個(gè)成員是tcbhead_ttcbhead_tdtv指向了一個(gè)dtv_t數(shù)組,該數(shù)組的大小隨著當(dāng)前程序載入的模塊多少而動(dòng)態(tài)變化。每一個(gè)模塊被載入時(shí),都有一個(gè)l_tls_modid,其直接作為dtv_t數(shù)組的下標(biāo)索引。tcbhead_t中的dtv實(shí)際指向的是dtv_t第二個(gè)元素,第一個(gè)元素用于記錄整個(gè)dtv_t數(shù)組有多少元素,第二個(gè)元素也做特殊使用,從第三個(gè)元素開始,才是用于存儲(chǔ)TLS變量。

一個(gè)dtv_t存儲(chǔ)的是一個(gè)模塊中所有TLS變量的地址,當(dāng)然這些TLS變量都會(huì)被放在連續(xù)的內(nèi)存空間里。dtv_t::pointer::val正是用于指向這塊內(nèi)存的指針。對(duì)于非動(dòng)態(tài)加載的模塊它指向的是線程堆棧的位置;否則指向動(dòng)態(tài)分配的內(nèi)存位置。

以上結(jié)構(gòu)用代碼描述為,

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是一個(gè)用于以模塊為單位存儲(chǔ)TLS變量的數(shù)組

實(shí)際代碼參看 /nptl/descr.h 及 nptl/sysdeps/x86_64/tls.h。

實(shí)驗(yàn)

使用g++ -o thread -g -Wall -lpthread -ldl thread.cpp編譯代碼,即在創(chuàng)建線程前加載了一個(gè).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]對(duì)應(yīng)著動(dòng)態(tài)加載的模塊,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數(shù)組大小之所以為17,可以參看代碼 /elf/dl-tls.c allocate_dtv

// dl_tls_max_dtv_idx 隨著載入模塊的增加而增加,載入1個(gè).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;

繼續(xù)上面的實(shí)驗(yàn),當(dāng)調(diào)用到.so中的function時(shí),其TLS被初始化,此時(shí)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

這個(gè)時(shí)候還可以看看dtv[1]中的內(nèi)容,正是指向了pthread前面的內(nèi)存位置:

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

結(jié)論:

  • 線程中TLS變量的存儲(chǔ)是以模塊為單位的

so模塊加載

這里也并不太需要查看dlopen等具體實(shí)現(xiàn),由于使用__thread來定義TLS變量,整個(gè)實(shí)現(xiàn)涉及到ELF加載器的一些細(xì)節(jié),深入下去內(nèi)容較多。這里直接通過實(shí)驗(yàn)的手段來了解一些實(shí)現(xiàn)即可。

上文已經(jīng)看到,在創(chuàng)建線程前如果動(dòng)態(tài)加載了.so,dtv數(shù)組的大小是會(huì)隨之增加的。如果是在線程創(chuàng)建后再載入.so呢?

使用g++ -o thread -g -Wall -lpthread -ldl thread.cpp -DTEST_DTV_EXPAND -DSO_CNT=1編譯程序,調(diào)試得到:

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時(shí),dtv數(shù)組大小并沒有新增,dtv[4]直接被拿來使用。

因?yàn)?code>dtv初始大小為16,那么當(dāng)載入的.so超過這個(gè)數(shù)字的時(shí)候會(huì)怎樣?

使用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被重新分配了內(nèi)存(0x6016a0 -> 0x6078a0)并做了擴(kuò)大。

以上得出結(jié)論:

  • 創(chuàng)建線程前dtv的大小會(huì)根據(jù)載入模塊數(shù)量決定
  • 創(chuàng)建線程后新載入的模塊會(huì)動(dòng)態(tài)擴(kuò)展dtv的大小(必要的時(shí)候)

pthread堆棧重用

allocate_stack中分配線程堆棧時(shí),有一個(gè)從緩存中取的操作:

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) // 根據(jù)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會(huì)把取出的pthread中的dtv重新初始化。注意 _dl_allocate_tls_init 中是根據(jù)模塊列表來初始化dtv數(shù)組的。

實(shí)驗(yàn)

當(dāng)一個(gè)線程退出后,它就可能被當(dāng)做cache被get_cached_stack取出復(fù)用。

使用g++ -o thread -g -Wall -lpthread -ldl thread.cpp -DTEST_CACHE_STACK編譯程序,運(yùn)行:

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

回顧BUG

當(dāng)新創(chuàng)建的線程復(fù)用了之前退出的線程堆棧時(shí),由于在_dl_allocate_tls_init中初始化dtv數(shù)組時(shí)是根據(jù)當(dāng)前載入的模塊數(shù)量而定。如果在這個(gè)時(shí)候模塊數(shù)已經(jīng)超過了這個(gè)復(fù)用的dtv數(shù)組大小,那么就會(huì)出現(xiàn)寫入非法的內(nèi)存。使用valgrind檢測(cè)就會(huì)得到本文開頭提到的結(jié)果。

由于dtv數(shù)組大小通常會(huì)稍微大點(diǎn),所以在新加載的模塊數(shù)量不夠多時(shí)程序還不會(huì)有問題。可以通過控制測(cè)試程序中SO_CNT的大小看看dtv中內(nèi)容的變化。

另外,我查看了下glibc的更新歷史,到目前為止(2.20)這個(gè)BUG還沒有修復(fù)。

參考文檔

posted on 2014-10-07 21:38 Kevin Lynx 閱讀(3671) 評(píng)論(1)  編輯 收藏 引用 所屬分類: c/c++

評(píng)論

# re: 淺析glibc中thread tls的一處bug 2014-10-08 09:37 Enic

linux各種坑  回復(fù)  更多評(píng)論   

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            欧美调教视频| 久久精品国产999大香线蕉| 香蕉av777xxx色综合一区| 亚洲毛片在线观看| 久久av红桃一区二区小说| 亚洲视频一二三| 欧美不卡在线视频| 欧美成人国产一区二区| 国产日产亚洲精品| 亚洲午夜激情| 亚洲影视在线| 欧美日韩国产综合一区二区| 欧美激情一区二区三区高清视频 | 欧美在线播放一区二区| 欧美久久电影| 亚洲日本中文| 亚洲美女色禁图| 欧美ed2k| 亚洲国产日韩综合一区| 在线看欧美日韩| 久久久精品国产99久久精品芒果| 久久国产精彩视频| 国产日韩在线一区| 久久www成人_看片免费不卡| 欧美一区二区在线播放| 国产精品永久入口久久久| 亚洲一区二区三区中文字幕| 亚洲女女女同性video| 欧美天堂亚洲电影院在线播放 | 美女日韩欧美| 亚洲成人在线免费| 欧美福利电影在线观看| 91久久久久久国产精品| 一本综合精品| 国产精品欧美久久久久无广告| 国产精品99久久久久久久vr| 午夜国产精品视频免费体验区| 国产精品乱码一区二三区小蝌蚪| 亚洲欧美欧美一区二区三区| 久久精品亚洲| 最新亚洲一区| 欧美日韩在线大尺度| 亚洲制服欧美中文字幕中文字幕| 欧美在线日韩| 亚洲国产第一| 国产精品xxx在线观看www| 亚洲综合色视频| 免费成人性网站| 一二三区精品| 国产亚洲va综合人人澡精品| 久久精品五月婷婷| 亚洲欧洲日本在线| 亚洲欧美日韩一区在线观看| 国产亚洲一区二区三区在线播放| 久久综合久久88| 亚洲精品黄色| 久久免费精品日本久久中文字幕| 亚洲国产精品成人精品| 国产精品99一区| 久久精品视频在线播放| 日韩亚洲欧美一区二区三区| 久久久天天操| 亚洲天堂成人在线观看| 激情久久久久久| 欧美午夜精品久久久久久久| 久久狠狠亚洲综合| 一区二区三区欧美在线| 猛干欧美女孩| 午夜精品久久久久久久99水蜜桃 | 亚洲欧美一区在线| 亚洲国产成人久久综合| 欧美在线视频观看| 日韩写真在线| 永久91嫩草亚洲精品人人| 欧美性做爰毛片| 久热re这里精品视频在线6| 亚洲先锋成人| 亚洲精品一区在线观看| 美日韩在线观看| 久久国产夜色精品鲁鲁99| 日韩一级免费观看| 亚洲成人在线视频播放| 国产亚洲一本大道中文在线| 欧美性猛交99久久久久99按摩| 噜噜噜91成人网| 久久成人免费电影| 亚洲一品av免费观看| 亚洲精品美女| 亚洲国产一区二区a毛片| 久久午夜视频| 久久久久久91香蕉国产| 午夜精品三级视频福利| 亚洲一区bb| 艳女tv在线观看国产一区| 亚洲国产精品一区二区久| 黄页网站一区| 曰韩精品一区二区| 国模精品娜娜一二三区| 国产农村妇女精品一二区| 国产精品mm| 欧美小视频在线观看| 欧美日韩国产页| 欧美日韩亚洲网| 欧美日韩精品一区二区在线播放 | 国产欧美日韩亚洲| 国产精品免费在线| 国产精品免费视频观看| 国产精品国产三级国产专播精品人| 欧美日本网站| 欧美三区不卡| 国产精品乱码久久久久久| 国产精品久久国产愉拍| 国产精品日韩一区二区| 国产午夜亚洲精品理论片色戒| 国产日韩欧美一区在线| 国产香蕉97碰碰久久人人| 国产自产高清不卡| 在线欧美日韩国产| 亚洲精选一区二区| 亚洲视频成人| 欧美一区2区三区4区公司二百| 欧美在线观看天堂一区二区三区| 久久国产精品久久精品国产| 久久综合九色| 亚洲黄色精品| 亚洲一区二区三区午夜| 欧美一级片在线播放| 欧美在线一级视频| 免费日韩成人| 国产精品红桃| 影音先锋亚洲视频| 妖精成人www高清在线观看| 亚洲欧美在线一区二区| 老司机精品视频一区二区三区| 欧美电影免费| 亚洲私人黄色宅男| 久久琪琪电影院| 欧美日韩免费一区二区三区视频 | 国内精品伊人久久久久av影院 | 午夜在线电影亚洲一区| 久久久国产精品一区二区中文| 欧美粗暴jizz性欧美20| 亚洲天堂av图片| 久久全国免费视频| 欧美日韩一区二区高清| 国产又爽又黄的激情精品视频| 亚洲人成啪啪网站| 小黄鸭精品aⅴ导航网站入口| 免费一级欧美片在线播放| 亚洲狼人综合| 久久精品欧美日韩精品| 欧美日韩亚洲综合一区| 激情久久综合| 午夜亚洲性色视频| 亚洲国产欧美国产综合一区| 亚洲综合色丁香婷婷六月图片| 欧美va天堂在线| 国产一区久久久| 亚洲性夜色噜噜噜7777| 亚洲第一区在线| 欧美在线啊v一区| 国产精品久久久久久超碰| 亚洲人www| 美国成人毛片| 午夜精品在线| 欧美视频日韩视频在线观看| 亚洲国产mv| 久久久精品tv| 午夜精品久久99蜜桃的功能介绍| 欧美精品综合| 亚洲人成小说网站色在线| 久久中文在线| 亚欧美中日韩视频| 国产精品欧美日韩久久| 一本色道久久综合精品竹菊| 猛男gaygay欧美视频| 欧美一区二区三区的| 国产精品亚洲а∨天堂免在线| 一区二区三区|亚洲午夜| 欧美激情综合色| 卡一卡二国产精品| 在线观看视频欧美| 久久美女性网| 久久精品一本| 国产综合色产在线精品| 久久国产精品一区二区| 小黄鸭精品aⅴ导航网站入口| 国产精品区免费视频| 亚洲欧美成人一区二区三区| 日韩一级在线| 国产精品二区在线| 亚洲欧美变态国产另类| 亚洲午夜视频在线| 国产精品亚洲第一区在线暖暖韩国| 亚洲欧美国产另类| 亚洲欧美国产日韩天堂区| 国产免费观看久久黄| 久久久久久噜噜噜久久久精品| 欧美一区二区女人| 影音先锋亚洲精品|