記一次tcmalloc分配內(nèi)存引起的coredump

字號:


    現(xiàn)象
    線上的服務(wù)出現(xiàn)coredump,堆棧為:
    #0 0x000000000045d145 in GetStackTrace(void**, int, int) ()
    #1 0x000000000045ec22 in tcmalloc::PageHeap::GrowHeap(unsigned long) ()
    #2 0x000000000045eeb3 in tcmalloc::PageHeap::New(unsigned long) ()
    #3 0x0000000000459ee8 in tcmalloc::CentralFreeList::Populate() ()
    #4 0x000000000045a088 in tcmalloc::CentralFreeList::FetchFromSpansSafe() ()
    #5 0x000000000045a10a in tcmalloc::CentralFreeList::RemoveRange(void**, void**, int) ()
    #6 0x000000000045c282 in tcmalloc::ThreadCache::FetchFromCentralCache(unsigned long, unsigned long) ()
    #7 0x0000000000470766 in tc_malloc ()
    #8 0x00007f75532cd4c2 in __conhash_get_rbnode (node=0x22c86870, hash=30)
    at build/release64/cm_sub/conhash/conhash_inter.c:88
    #9 0x00007f75532cd76e in __conhash_add_replicas (conhash=0x24fbc7e0, iden=<value optimized out>)
    at build/release64/cm_sub/conhash/conhash_inter.c:45
    #10 0x00007f75532cd1fa in conhash_add_node (conhash=0x24fbc7e0, iden=0) at build/release64/cm_sub/conhash/conhash.c:72
    #11 0x00007f75532c651b in cm_sub::TopoCluster::initLBPolicyInfo (this=0x2593a400)
    at build/release64/cm_sub/topo_cluster.cpp:114
    #12 0x00007f75532cad73 in cm_sub::TopoClusterManager::processClusterMapTable (this=0xa219e0, ref=0x267ea8c0)
    at build/release64/cm_sub/topo_cluster_manager.cpp:396
    #13 0x00007f75532c5a93 in cm_sub::SubRespMsgProcess::reinitCluster (this=0x9c2f00, msg=0x4e738ed0)
    at build/release64/cm_sub/sub_resp_msg_process.cpp:157
    ...
    查看了應(yīng)用層相關(guān)數(shù)據(jù)結(jié)構(gòu),基本數(shù)據(jù)都是沒有問題的。所以最初懷疑是tcmalloc內(nèi)部維護了錯誤的內(nèi)存,在分配內(nèi)存時出錯,這個堆棧只是問題的表象。幾天后,線上的另一個服務(wù),基于同樣的庫,也core了,堆棧還是一樣的。
    最初定位問題都是從最近更新的東西入手,包括依賴的server環(huán)境,但都沒有明顯的問題,所以最后只能從core的直接原因入手。
    分析GetStackTrace
    確認core的詳細位置:
    # core在該指令
    0x000000000045d145 <_Z13GetStackTracePPvii+21>: mov 0x8(%rax),%r9
    (gdb) p/x $rip # core 的指令位置
    $9 = 0x45d145
    (gdb) p/x $rax
    $10 = 0x4e73aa58
    (gdb) x/1a $rax+0x8 # rax + 8 = 0x4e73aa60
    0x4e73aa60: 0x0
    該指令嘗試從[0x4e73aa60]處讀取內(nèi)容,然后出錯,這個內(nèi)存單元不可讀。但是具體這個指令在代碼中是什么意思,需要將這個指令對應(yīng)到代碼中。獲取tcmalloc的源碼,發(fā)現(xiàn)GetStackTrace根據(jù)編譯選項有很多實現(xiàn),所以這里選擇最可能的實現(xiàn),然后對比匯編以確認代碼是否匹配。最初選擇的是stacktrace_x86-64-inl.h,后來發(fā)現(xiàn)完全不匹配,又選擇了stacktrace_x86-inl.h。這個實現(xiàn)版本里也有對64位平臺的支持。
    stacktrace_x86-inl.h里使用了一些宏來生成函數(shù)名和參數(shù),精簡后代碼大概為:
    int GET_STACK_TRACE_OR_FRAMES {
    void **sp;
    unsigned long rbp;
    __asm__ volatile ("mov %%rbp, %0" : "=r" (rbp));
    sp = (void **) rbp;
    int n = 0;
    while (sp && n < max_depth) {
    if (*(sp+1) == reinterpret_cast<void *>(0)) {
    break;
    }
    void **next_sp = NextStackFrame<!IS_STACK_FRAMES, IS_WITH_CONTEXT>(sp, ucp);
    if (skip_count > 0) {
    skip_count--;
    } else {
    result[n] = *(sp+1);
    n++;
    }
    sp = next_sp;
    }
    return n;
    }
    NextStackFrame是一個模板函數(shù),包含一大堆代碼,精簡后非常簡單:
    template<bool STRICT_UNWINDING, bool WITH_CONTEXT>
    static void **NextStackFrame(void **old_sp, const void *uc) {
    void **new_sp = (void **) *old_sp;
    if (STRICT_UNWINDING) {
    if (new_sp <= old_sp) return NULL;
    if ((uintptr_t)new_sp - (uintptr_t)old_sp > 100000) return NULL;
    } else {
    if (new_sp == old_sp) return NULL;
    if ((new_sp > old_sp)
    && ((uintptr_t)new_sp - (uintptr_t)old_sp > 1000000)) return NULL;
    }
    if ((uintptr_t)new_sp & (sizeof(void *) - 1)) return NULL;
    return new_sp;
    }
    上面這個代碼到匯編的對比過程還是花了些時間,其中匯編中出現(xiàn)的一些常量可以大大縮短對比時間,例如上面出現(xiàn)了100000,匯編中就有:
    0x000000000045d176 <_Z13GetStackTracePPvii+70>: cmp $0x186a0,%rbx # 100000=0x186a0
    注意NextStackFrame中的 if (STRICT_UNWINDING)使用的是模板參數(shù),這導(dǎo)致生成的代碼中根本沒有else部分,也就沒有1000000這個常量
    在對比代碼的過程中,可以知道關(guān)鍵的幾個寄存器、內(nèi)存位置對應(yīng)到代碼中的變量,從而可以還原core時的現(xiàn)場環(huán)境。分析過程中不一定要從第一行匯編讀,可以從較明顯的位置讀,從而還原整個代碼,函數(shù)返回指令、跳轉(zhuǎn)指令、比較指令、讀內(nèi)存指令、參數(shù)寄存器等都是比較明顯對應(yīng)的地方。
    另外注意GetStackTrace在RecordGrowth中調(diào)用,傳入了3個參數(shù):
    GetStackTrace(t->stack, kMaxStackDepth-1, 3); // kMaxStackDepth = 31
    以下是我分析的簡單注解:
    (gdb) disassemble
    Dump of assembler code for function _Z13GetStackTracePPvii:
    0x000000000045d130 <_Z13GetStackTracePPvii+0>: push %rbp
    0x000000000045d131 <_Z13GetStackTracePPvii+1>: mov %rsp,%rbp
    0x000000000045d134 <_Z13GetStackTracePPvii+4>: push %rbx
    0x000000000045d135 <_Z13GetStackTracePPvii+5>: mov %rbp,%rax
    0x000000000045d138 <_Z13GetStackTracePPvii+8>: xor %r8d,%r8d
    0x000000000045d13b <_Z13GetStackTracePPvii+11>: test %rax,%rax
    0x000000000045d13e <_Z13GetStackTracePPvii+14>: je 0x45d167 <_Z13GetStackTracePPvii+55>
    0x000000000045d140 <_Z13GetStackTracePPvii+16>: cmp %esi,%r8d # while ( .. max_depth > n ?
    0x000000000045d143 <_Z13GetStackTracePPvii+19>: jge 0x45d167 <_Z13GetStackTracePPvii+55>
    0x000000000045d145 <_Z13GetStackTracePPvii+21>: mov 0x8(%rax),%r9 # 關(guān)鍵位置:*(sp+1) -> r9, rax 對應(yīng) sp變量
    0x000000000045d149 <_Z13GetStackTracePPvii+25>: test %r9,%r9 # *(sp+1) == 0 ?
    0x000000000045d14c <_Z13GetStackTracePPvii+28>: je 0x45d167 <_Z13GetStackTracePPvii+55>
    0x000000000045d14e <_Z13GetStackTracePPvii+30>: mov (%rax),%rcx # new_sp = *old_sp,這里已經(jīng)是NextStackFrame的代碼
    0x000000000045d151 <_Z13GetStackTracePPvii+33>: cmp %rcx,%rax # new_sp <= old_sp ?
    0x000000000045d154 <_Z13GetStackTracePPvii+36>: jb 0x45d170 <_Z13GetStackTracePPvii+64> # new_sp > old_sp 跳轉(zhuǎn)
    0x000000000045d156 <_Z13GetStackTracePPvii+38>: xor %ecx,%ecx
    0x000000000045d158 <_Z13GetStackTracePPvii+40>: test %edx,%edx # skip_count > 0 ?
    0x000000000045d15a <_Z13GetStackTracePPvii+42>: jle 0x45d186 <_Z13GetStackTracePPvii+86>
    0x000000000045d15c <_Z13GetStackTracePPvii+44>: sub $0x1,%edx # skip_count--
    0x000000000045d15f <_Z13GetStackTracePPvii+47>: mov %rcx,%rax
    0x000000000045d162 <_Z13GetStackTracePPvii+50>: test %rax,%rax # while (sp ?
    0x000000000045d165 <_Z13GetStackTracePPvii+53>: jne 0x45d140 <_Z13GetStackTracePPvii+16>
    0x000000000045d167 <_Z13GetStackTracePPvii+55>: pop %rbx
    0x000000000045d168 <_Z13GetStackTracePPvii+56>: leaveq
    0x000000000045d169 <_Z13GetStackTracePPvii+57>: mov %r8d,%eax # r8 存儲了返回值,r8=n
    0x000000000045d16c <_Z13GetStackTracePPvii+60>: retq # return n
    0x000000000045d16d <_Z13GetStackTracePPvii+61>: nopl (%rax)
    0x000000000045d170 <_Z13GetStackTracePPvii+64>: mov %rcx,%rbx
    0x000000000045d173 <_Z13GetStackTracePPvii+67>: sub %rax,%rbx # offset = new_sp - old_sp
    0x000000000045d176 <_Z13GetStackTracePPvii+70>: cmp $0x186a0,%rbx # offset > 100000 ?
    0x000000000045d17d <_Z13GetStackTracePPvii+77>: ja 0x45d156 <_Z13GetStackTracePPvii+38> # return NULL
    0x000000000045d17f <_Z13GetStackTracePPvii+79>: test $0x7,%cl # new_sp & (sizeof(void*) - 1)
    0x000000000045d182 <_Z13GetStackTracePPvii+82>: je 0x45d158 <_Z13GetStackTracePPvii+40>
    0x000000000045d184 <_Z13GetStackTracePPvii+84>: jmp 0x45d156 <_Z13GetStackTracePPvii+38>
    0x000000000045d186 <_Z13GetStackTracePPvii+86>: movslq %r8d,%rax # rax = n
    0x000000000045d189 <_Z13GetStackTracePPvii+89>: add $0x1,%r8d # n++
    0x000000000045d18d <_Z13GetStackTracePPvii+93>: mov %r9,(%rdi,%rax,8)# 關(guān)鍵位置:result[n] = *(sp+1)
    0x000000000045d191 <_Z13GetStackTracePPvii+97>: jmp 0x45d15f <_Z13GetStackTracePPvii+47>
    分析過程比較耗時,同時還可以分析下GetStackTrace函數(shù)的實現(xiàn)原理,其實就是利用RBP寄存器不斷回溯,從而得到整個調(diào)用堆棧各個函數(shù)的地址(嚴(yán)格來說是返回地址)。簡單示意下函數(shù)調(diào)用中RBP的情況:
    ...
    saved registers # i.e push rbx
    local variabes # i.e sub 0x10, rsp
    return address # call xxx
    last func RBP # push rbp; mov rsp, rbp
    saved registers
    local variables
    return address
    last func RBP
    ... # rsp
    總之,一般情況下,任何一個函數(shù)中,RBP寄存器指向了當(dāng)前函數(shù)的?;?,該?;分杏执鎯α苏{(diào)用者的?;?,同時該?;非懊孢€存儲了調(diào)用者的返回地址。所以,GetStackTrace的實現(xiàn),簡單來說大概就是:
    sp = rbp // 取得當(dāng)前函數(shù)GetStackTrace的?;?BR>    while (n < max_depth) {
    new_sp = *sp
    result[n] = *(new_sp+1)
    n++
    }
    以上,最終就知道了以下關(guān)鍵信息:
    r8 對應(yīng)變量 n,表示當(dāng)前取到第幾個棧幀了
    rax 對應(yīng)變量 sp,代碼core在 *(sp+1)
    rdi 對應(yīng)變量 result,用于存儲取得的各個地址
    然后可以看看現(xiàn)場是怎樣的:
    (gdb) x/10a $rdi
    0x1ffc9b98: 0x45a088 <_ZN8tcmalloc15CentralFreeList18FetchFromSpansSafeEv+40> 0x45a10a <_ZN8tcmalloc15CentralFreeList11RemoveRangeEPPvS2_i+106>
    0x1ffc9ba8: 0x45c282 <_ZN8tcmalloc11ThreadCache21FetchFromCentralCacheEmm+114> 0x470766 <tc_malloc+790>
    0x1ffc9bb8: 0x7f75532cd4c2 <__conhash_get_rbnode+34> 0x0
    0x1ffc9bc8: 0x0 0x0
    0x1ffc9bd8: 0x0 0x0
    (gdb) p/x $r8
    $3 = 0x5
    (gdb) p/x $rax
    $4 = 0x4e73aa58
    小結(jié):
    GetStackTrace在取調(diào)用__conhash_get_rbnode的函數(shù)時出錯,取得了5個函數(shù)地址。當(dāng)前使用的RBP為0x4e73aa58。
    錯誤的RBP
    RBP也是從堆棧中取出來的,既然這個地址有問題,首先想到的就是有代碼局部變量/數(shù)組寫越界。例如sprintf的使用。而且,一般寫越界破壞堆棧,都可能是把調(diào)用者的堆棧破壞了,例如:
    char s[32];
    memcpy(s, p, 1024);
    因為寫入都是從低地址往高地址寫,而調(diào)用者的堆棧在高地址。當(dāng)然,也會遇到寫壞調(diào)用者的調(diào)用者的堆棧,也就是跨棧幀越界寫,例如以前遇到的:
    len = vsnprintf(buf, sizeof(buf), fmt, wtf-long-string);
    buf[len] = 0;
    __conhash_get_rbnode的RBP是在tcmalloc的堆棧中取的:
    (gdb) f 7
    #7 0x0000000000470766 in tc_malloc ()
    (gdb) x/10a $rsp
    0x4e738b80: 0x4e73aa58 0x22c86870
    0x4e738b90: 0x4e738bd0 0x85
    0x4e738ba0: 0x4e73aa58 0x7f75532cd4c2 <__conhash_get_rbnode+34> # 0x4e73aa58
    所以這里就會懷疑是tcmalloc這個函數(shù)里有把堆棧破壞,這個時候就是讀代碼,看看有沒有疑似危險的地方,未果。這里就陷入了僵局,懷疑又遇到了跨棧幀破壞的情況,這個時候就只能__conhash_get_rbnode調(diào)用棧中周圍的函數(shù)翻翻,例如調(diào)用__conhash_get_rbnode的函數(shù)__conhash_add_replicas中恰好有字符串操作:
    void __conhash_add_replicas(conhash_t *conhash, int32_t iden)
    {
    node_t* node = __conhash_create_node(iden, conhash->replica);
    ...
    char buf[buf_len]; // buf_len = 64
    ...
    snprintf(buf, buf_len, VIRT_NODE_HASH_FMT, node->iden, i);
    uint32_t hash = conhash->cb_hashfunc(buf);
    if(util_rbtree_search(&(conhash->vnode_tree), hash) == NULL)
    {
    util_rbtree_node_t* rbnode = __conhash_get_rbnode(node, hash);
    ...
    這段代碼最終發(fā)現(xiàn)是沒有問題的,這里又耗費了不少時間。后來發(fā)現(xiàn)若干個函數(shù)里的RBP都有點奇怪,這個調(diào)用棧比較正常的范圍是:0x4e738c90
    (gdb) f 8
    #8 0x00007f75532cd4c2 in __conhash_get_rbnode (node=0x22c86870, hash=30)
    (gdb) p/x $rbp
    $6 = 0x4e73aa58 # 這個還不算特別可疑
    (gdb) f 9
    #9 0x00007f75532cd76e in __conhash_add_replicas (conhash=0x24fbc7e0, iden=<value optimized out>)
    (gdb) p/x $rbp
    $7 = 0x4e738c60 # 這個也不算特別可疑
    (gdb) f 10
    #10 0x00007f75532cd1fa in conhash_add_node (conhash=0x24fbc7e0, iden=0) at build/release64/cm_sub/conhash/conhash.c:72
    (gdb) p/x $rbp # 可疑
    $8 = 0x0
    (gdb) f 11
    #11 0x00007f75532c651b in cm_sub::TopoCluster::initLBPolicyInfo (this=0x2593a400)
    (gdb) p/x $rbp # 可疑
    $9 = 0x2598fef0
    為什么很多函數(shù)中RBP都看起來不正常? 想了想真要是代碼里把堆棧破壞了,這錯誤得發(fā)生得多巧妙?
    錯誤RBP的來源
    然后轉(zhuǎn)機來了,腦海中突然閃出-fomit-frame-pointer。編譯器生成的代碼中是可以不需要?;分羔樀?,也就是RBP寄存器不作為棧基址寄存器。大部分函數(shù)或者說開啟了frame-pointer的函數(shù),其函數(shù)頭都會有以下指令:
    push %rbp
    mov %rsp,%rbp
    ...
    表示保存調(diào)用者的?;返綏V?,以及設(shè)置自己的?;贰?聪耞_conhash系列函數(shù);
    Dump of assembler code for function __conhash_get_rbnode:
    0x00007f75532cd4a0 <__conhash_get_rbnode+0>: mov %rbx,-0x18(%rsp)
    0x00007f75532cd4a5 <__conhash_get_rbnode+5>: mov %rbp,-0x10(%rsp)
    ...
    這個庫是單獨編譯的,沒有顯示指定-fno-omit-frame-pointer,查閱gcc手冊,o2優(yōu)化是開啟了omit-frame-pinter 的。
    在沒有RBP的情況下,tcmalloc的GetStackTrace嘗試讀RBP取獲取調(diào)用返回地址,自然是有問題的。但是,如果整個調(diào)用棧中的函數(shù),要么有RBP,要么沒有RBP,那么GetStackTrace取出的結(jié)果最多就是跳過一些棧幀,不會出錯。 除非,這中間的某個函數(shù)把RBP寄存器另作他用(編譯器省出這個寄存器肯定是要另作他用的)。所以這里繼續(xù)追查這個錯誤地址0x4e73aa58的來源。
    來源已經(jīng)比較明顯,肯定是__conhash_get_rbnode中設(shè)置的,因為這個函數(shù)的RBP是在被調(diào)用者tcmalloc中保存的。
    Dump of assembler code for function __conhash_get_rbnode:
    0x00007f75532cd4a0 <__conhash_get_rbnode+0>: mov %rbx,-0x18(%rsp)
    0x00007f75532cd4a5 <__conhash_get_rbnode+5>: mov %rbp,-0x10(%rsp)
    0x00007f75532cd4aa <__conhash_get_rbnode+10>: mov %esi,%ebp # 改寫了RBP
    0x00007f75532cd4ac <__conhash_get_rbnode+12>: mov %r12,-0x8(%rsp)
    0x00007f75532cd4b1 <__conhash_get_rbnode+17>: sub $0x18,%rsp
    0x00007f75532cd4b5 <__conhash_get_rbnode+21>: mov %rdi,%r12
    0x00007f75532cd4b8 <__conhash_get_rbnode+24>: mov $0x30,%edi
    0x00007f75532cd4bd <__conhash_get_rbnode+29>: callq 0x7f75532b98c8 <> # 調(diào)用tcmalloc,匯編到這里即可
    這里打印RSI寄存器的值可能會被誤導(dǎo),因為任何時候打印寄存器的值可能都是錯的,除非它有被顯示保存。不過這里可以看出RSI的值來源于參數(shù)(RSI對應(yīng)第二個參數(shù)):
    void __conhash_add_replicas(conhash_t *conhash, int32_t iden)
    {
    node_t* node = __conhash_create_node(iden, conhash->replica);
    ...
    char buf[buf_len]; // buf_len = 64
    ...
    snprintf(buf, buf_len, VIRT_NODE_HASH_FMT, node->iden, i);
    uint32_t hash = conhash->cb_hashfunc(buf); // hash值由一個字符串哈希函數(shù)計算
    if(util_rbtree_search(&(conhash->vnode_tree), hash) == NULL)
    {
    util_rbtree_node_t* rbnode = __conhash_get_rbnode(node, hash); // hash值
    ...
    追到__conhash_add_replicas:
    0x00007f75532cd764 <__conhash_add_replicas+164>: mov %ebx,%esi # 來源于rbx
    0x00007f75532cd766 <__conhash_add_replicas+166>: mov %r15,%rdi
    0x00007f75532cd769 <__conhash_add_replicas+169>: callq 0x7f75532b9e48 <>
    (gdb) p/x $rbx
    $11 = 0x4e73aa58
    (gdb) p/x hash
    $12 = 0x4e73aa58 # 0x4e73aa58
    找到了0x4e73aa58的來源。這個地址值竟然是一個字符串哈希算法算出來的!這里還可以看看這個字符串的內(nèi)容:
    (gdb) x/1s $rsp
    0x4e738bd0: "conhash-00000-00133"
    這個碉堡的哈希函數(shù)是conhash_hash_def。
    coredump的條件
    以上,既然只要某個庫omit-frame-pointer,那tcmalloc就可能出錯,為什么發(fā)生的頻率并不高呢?這個可以回到GetStackTrace尤其是NextStackFrame的實現(xiàn),其中包含了幾個合法RBP的判定:
    if (new_sp <= old_sp) return NULL; // 上一個棧幀的RBP肯定比當(dāng)前的大
    if ((uintptr_t)new_sp - (uintptr_t)old_sp > 100000) return NULL; // 指針值范圍還必須在100000內(nèi)
    ...
    if ((uintptr_t)new_sp & (sizeof(void *) - 1)) return NULL; // 由于本身保存的是指針,所以還必須是sizeof(void*)的整數(shù)倍,對齊
    有了以上條件,才使得這個core幾率變得很低。
    總結(jié)
    最后,如果你很熟悉tcmalloc,整個問題估計就被秒解了:tcmalloc INSTALL
    附
    另外附上另一個有意思的東西。
    在分析__conhash_add_replicas時,其內(nèi)定義了一個64字節(jié)的字符數(shù)組,查看其堆棧:
    (gdb) x/20a $rsp
    0x4e738bd0: 0x2d687361686e6f63 0x30302d3030303030 # 這些是字符串conhash-00000-00133
    0x4e738be0: 0x333331 0x0
    0x4e738bf0: 0x0 0x7f75532cd69e <__conhash_create_node+78>
    0x4e738c00: 0x24fbc7e0 0x4e738c60
    0x4e738c10: 0x24fbc7e0 0x7f75532cd6e3 <__conhash_add_replicas+35>
    0x4e738c20: 0x0 0x24fbc7e8
    0x4e738c30: 0x4e738c20 0x24fbc7e0
    0x4e738c40: 0x22324360 0x246632c0
    0x4e738c50: 0x0 0x0
    0x4e738c60: 0x0 0x7f75532cd1fa <conhash_add_node+74>
    最開始我覺得buf占64字節(jié),也就是整個[0x4e738bd0, 0x4e738c10)內(nèi)存,但是這塊內(nèi)存里居然有函數(shù)地址,這一度使我懷疑這里有問題。后來醒悟這些地址是定義buf前調(diào)用__conhash_create_node產(chǎn)生的,調(diào)用過程中寫到堆棧里,調(diào)用完后棧指針改變,但并不需要清空棧中的內(nèi)容。