技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 算法 --> 排错经历:全局变量被多次析构

排错经历:全局变量被多次析构

浏览:1806次  出处信息

   我们team有一套C++写的server程序,最近发现它在每次退出的时候会崩溃,core dump文件的栈如下:

   (gdb) bt
#0  0x0000003ea4e32925 in raise () from /lib64/libc.so.6
#1  0x0000003ea4e34105 in abort () from /lib64/libc.so.6
#2  0x0000003ea4e70837 in __libc_message () from /lib64/libc.so.6
#3  0x0000003ea4e76166 in malloc_printerr () from /lib64/libc.so.6
#4  0x0000003ea729d4c9 in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() ()
   from /usr/lib64/libstdc++.so.6
#5  0x0000003ea4e35e22 in exit () from /lib64/libc.so.6
#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

建议继续学习:

  1. C语言全局变量那些事儿    (阅读:3740)
  2. 关于全局变量不能全局的问题    (阅读:2671)
  3. c语言全局变量和局部变量问题汇总    (阅读:2646)
  4. C++讲解    (阅读:1944)
  5. jRaiser与jQuery的冲突问题    (阅读:1940)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1