咱們team有一套C++寫的server程序,最近發現它在每次退出的時候會崩潰,core dump文件的棧以下:html
(gdb) btlinux
#0 0x0000003ea4e32925 in raise () from /lib64/libc.so.6c++
#1 0x0000003ea4e34105 in abort () from /lib64/libc.so.6shell
#2 0x0000003ea4e70837 in __libc_message () from /lib64/libc.so.6服務器
#3 0x0000003ea4e76166 in malloc_printerr () from /lib64/libc.so.6session
#4 0x0000003ea729d4c9 in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() ()app
from /usr/lib64/libstdc++.so.6函數
#5 0x0000003ea4e35e22 in exit () from /lib64/libc.so.6post
#6 0x0000003ea4e1ed24 in __libc_start_main () from /lib64/libc.so.6優化
#7 0x0000000000400629 in _start ()
下面介紹一下我是如何找到出問題的代碼。
請注意,由於編譯器優化的緣故,這個棧是不完整的。安裝完調試符號後,棧應該是這樣:
(gdb) bt
#0 0x0000003ea4e32925 in raise (sig=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:64
#1 0x0000003ea4e34105 in abort () at abort.c:92
#2 0x0000003ea4e70837 in __libc_message (do_abort=2, fmt=0x3ea4f58aa0 「*** glibc detected *** %s: %s: 0x%s ***\n」)
at ../sysdeps/unix/sysv/linux/libc_fatal.c:198
#3 0x0000003ea4e76166 in malloc_printerr (action=3, str=0x3ea4f58d48 「double free or corruption (fasttop)」,
ptr=<value optimized out>) at malloc.c:6336
#4 0x0000003ea729d4c9 in _M_dispose (this=<value optimized out>, __in_chrg=<value optimized out>)
at /usr/src/debug/gcc-4.4.7-20120601/obj-x86_64-redhat-linux/x86_64-redhat-linux/libstdc++-v3/include/bits/basic_string.h:236
#5 std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string (this=<value optimized out>,
__in_chrg=<value optimized out>)
at /usr/src/debug/gcc-4.4.7-20120601/obj-x86_64-redhat-linux/x86_64-redhat-linux/libstdc++-v3/include/bits/basic_string.h:503
#6 0x0000003ea4e35e22 in __run_exit_handlers (status=0) at exit.c:78
#7 exit (status=0) at exit.c:100
#8 0x0000003ea4e1ed24 in __libc_start_main (main=0x4006f0 <main(int, char**)>, argc=1, ubp_av=0x7fffffffe488,
init=<value optimized out>, fini=<value optimized out>, rtld_fini=<value optimized out>, stack_end=0x7fffffffe478)
at libc-start.c:258
#9 0x0000000000400629 in _start ()
惋惜咱們服務器上的glibc是自定製的,找不到調試符號。因此我就只好黑燈瞎火的搞。崩潰發生在main函數返回以後。exit()函數執行 全局變量的析構,而後就崩潰了。看上去應該跟內存的釋放有關,可是我即使用valgrind也得不到任何有用的信息。只能知道是同一塊內存被釋放了兩次, 並且這是一個std::string,並且是個全局變量。可是咱們的代碼有幾十萬行,從何查起啊…… 因而首要任務是,先找到這個std::string的指針以及它所存的字符串的內容。
因而我首先切換到和std::basic_string相關的一楨
(gdb) f 5
#5 std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string (this=<value optimized out>,
__in_chrg=<value optimized out>)
at /usr/src/debug/gcc-4.4.7-20120601/obj-x86_64-redhat-linux/x86_64-redhat-linux/libstdc++-v3/include/bits/basic_string.h:503
503 { _M_rep()->_M_dispose(this->get_allocator()); }
(gdb) p *this
No symbol 「this」 in current context.
惋惜this指針被優化掉了。若是我能找到this指針的值,就能直接用gdb的info sym命令獲得symbol name,從而得知是哪一個變量出錯了。無論怎樣,先在這斷下來再說。
可是每次我嘗試給這個函數加斷點的時候,gdb就崩潰了
../../gdb/breakpoint.c:5721: internal-error: set_raw_breakpoint: Assertion `sal.pspace != NULL’ failed.
A problem internal to GDB has been detected,
further debugging may prove unreliable.
Quit this debugging session? (y or n) y
雖而後來我摸索出來了怎麼不讓gdb崩潰也能加斷點的辦法(直接b *addr),但那是後話了。就算我在這裏加了斷點,也很難設置條件。這個函數在exit中被調用幾百次呢。
因而我就在它的下一層的函數加了斷點
(gdb) b _ZNSs4_Rep10_M_destroyERKSaIcE
Breakpoint 1 at 0x3ea729c320
而後每次運行到這裏的時候,能夠看見this指針以及它的內容
Breakpoint 1, std::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Rep::_M_destroy (this=0x601e10,
__a=…)
at /usr/src/debug/gcc-4.4.7-20120601/obj-x86_64-redhat-linux/x86_64-redhat-linux/libstdc++-v3/include/bits/basic_string.tcc:450
450 _Raw_bytes_alloc(__a).deallocate(reinterpret_cast<char*>(this), __size);
(gdb) p *this
$1 = {<std::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Rep_base> = {_M_length = 3,
_M_capacity = 3, _M_refcount = -1}, static _S_max_size = 4611686018427387897, static _S_terminal = 0 ‘\000’,
static _S_empty_rep_storage = {0, 0, 0, 0}}
_M_refcount是一個引用計數器。正常狀況下,走到這裏時_M_refcount應該等於-1。若是發生了重複析構,那麼_M_refcount就不等於-1。我想讓進程停在異常發生的時候,惋惜這個函數被調用的太頻繁了。因此我給它加個條件斷點來找異常的狀況。
(gdb) b _ZNSs4_Rep10_M_destroyERKSaIcE if this->_M_refcount==-2
結果失敗了。gdb運行的時候說
Error in testing breakpoint condition:
value has been optimized out
因而我把這個對象的內存打出來,找出這個成員變量的偏移地址:
(gdb) x/16 this
0x601e10: 3 0 3 0
0x601e20: -1 0 7895160 0
0x601e30: 0 0 131537 0
0x601e40: 0 0 0 0
從上面的輸出能夠看出,它的地址和this指針相差4個int的大小。由於this指針存放在$edi中,因此this->_M_refcount的位置就是(int*)$edi+4。
因此上述的條件「if this->_M_refcount==-2」就能夠從新爲「if *((int*)$edi+4)!=-1」
(gdb) b _ZNSs4_Rep10_M_destroyERKSaIcE if *((int*)$edi+4)!=-1
而後成功捕獲到了
Breakpoint 12, std::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Rep::_M_destroy (this=0x4b4b510, __a=…) at /usr/src/debug/gcc-4.4.7-20120601/obj-x86_64-redhat-linux/x86_64-redhat-linux/libstdc++-v3/include/bits/basic_string.tcc:450
450_Raw_bytes_alloc(__a).deallocate(reinterpret_cast<char*>(this), __size);
$4201 = {
<std::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Rep_base> = {
_M_length = 78993104,
_M_capacity = 30,
_M_refcount = -2
},
members of std::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Rep:
static _S_max_size = 4611686018427387897,
static _S_terminal = 0 ‘\000’,
static _S_empty_rep_storage = {0, 0, 0, 0}
}
可是字符串的內容在哪呢?查了下代碼,發現它就在Rep這個對象的後面。能夠直接用gdb的x命令打印,可是爲了打印的更漂亮點,把這段內存像金山遊俠那樣打印出來,我在網上找到了一個辦法
首先定義一個名爲xxd的函數
(gdb) define xxd
Redefine command 「xxd」? (y or n) y
Type commands for definition of 「xxd」.
End with a line saying just 「end」.
>dump binary memory dump.bin $arg0 ((char*)$arg0)+$arg1
>shell xxd dump.bin
>end
第一個參數是要打印的內存的起始地址,第二個參數是內存區的長度。
而後調用這個函數。
(gdb) xxd this 64
0000000: 0000 0000 0000 0000 1e00 0000 0000 0000 ................
0000010: feff ffff 0000 0000 6170 706c 6963 6174 ........applicat
0000020: 696f 6e2f 786d 6c3b 2063 6861 7273 6574 ion/xml; charset
0000030: 3d55 5446 2d38 0000 b101 0200 0000 0000 =UTF-8..........
一目瞭然,是」application/xml; charset=UTF-8」這個字符串被析構了兩次。而後去代碼中grep。惋惜,若是能直接找到string的地址的話,用info sym (addr) 就能夠直接知道符號名了,不用去代碼中grep。
下面我把出問題的代碼精簡出來,給你們看看:
main.cpp:它只管載入兩個so,其它什麼都不作
#include <stdio.h> #include <dlfcn.h> int main(int argc,char* argv[]){ void* h1=dlopen("./libt1.so",RTLD_NOW|RTLD_GLOBAL); if (!h1) { fprintf(stderr, "%s:%d load t1 fail %s\n", __FILE__,(int)__LINE__,dlerror()); return -1; } void* h2=dlopen("./libt2.so",RTLD_NOW|RTLD_GLOBAL); if (!h2) { fprintf(stderr, "%s:%d load t2 fail %s\n", __FILE__,(int)__LINE__,dlerror()); dlclose(h1); return -1; } return 0; }
libt1.cpp: libt1.so的源文件
#include <string> std::string msg("application/xml; charset=UTF-8");
libt2.cpp: libt2.so的源文件,和libt1.cpp徹底相同
編譯命令
$ g++ -g3 -o t main.cpp -ldl
$ g++ -O2 -fPIC -shared -g3 -o libt1.so libt1.cpp
$ g++ -O2 -fPIC -shared -g3 -o libt2.so libt1.cpp
而後執行主程序,每次必崩潰。
$ ./t
Aborted (core dumped)
這是由於,msg這個變量是一個global的變量,當解析這個符號的時候,後加載的so會覆蓋前面。而且,每一個so在加載的時候,會向 exit函數註冊一個hook,由於這個全局變量須要在main函數退出後被析構。exit函數的hooks是一個鏈表。這兩個so各在其中註冊了一條。 因此,當exit函數執行的時候,第二個so的msg這個變量所指向的內存會被析構兩次。我講的不是很清楚,你能夠按照上面的代碼實驗一下,很快的。
This article is from: https://www.sunchangming.com/blog/post/4648.html