Linux调用backtrack函数打印程序崩溃时的调用堆栈

Linux调用backtrack函数打印程序崩溃时的调用堆栈

(2011-09-06 10:40:55)
转载
标签:

it

分类: gdb
可以给自己的程序都加上这个东西,便于快速的找到错误吧,看到别人都是这么用的
#include
#include
#include
#include


//signal 函数用法参考http://www.kernel.org/doc/man-pages/online/pages/man2/signal.2.html
//backtrace ,backtrace_symbols函数用法参考 http://www.kernel.org/doc/man-pages/online/pages/man3/backtrace.3.html

static void WidebrightSegvHandler(int signum) {
  void *array[10];
  size_t size;
  char **strings;
  size_t i, j;

  signal(signum, SIG_DFL);

  size = backtrace (array, 10);
  strings = (char **)backtrace_symbols (array, size);

  fprintf(stderr, "widebright received SIGSEGV! Stack trace:\n");
  for (i = 0; i < size; i++) {
      fprintf(stderr, "%d %s \n",i,strings[i]);
  }

  free (strings);
  exit(1);
}

int invalide_pointer_error(char * p)
{
  *p = 'd'; //让这里出现一个访问非法指针的错误
  return 0;
}


void error_2(char * p)
{
  invalide_pointer_error(p);
}

void error_1(char * p)
{
    error_2(p);
}

void error_0(char * p)
{
    error_1(p);
}





int main()
{

  //设置 信好的处理函数,各种信号的定义见http://www.kernel.org/doc/man-pages/online/pages/man7/signal.7.html
  signal(SIGSEGV, WidebrightSegvHandler); // SIGSEGV      11      Core    Invalid memory reference
  signal(SIGABRT, WidebrightSegvHandler); // SIGABRT      6      Core    Abort signal from


  char *a = NULL;
  error_0(a);
  exit(0);

}

widebright@widebright:~/桌面$ gcc main.c
widebright@widebright:~/桌面$ ./a.out
widebright received SIGSEGV! Stack trace:
0 ./a.out [0x8048580]
1 [0xb807a400]
2 ./a.out [0x8048636]
3 ./a.out [0x8048649]
4 ./a.out [0x804865c]
5 ./a.out [0x80486a9]
6 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5) [0xb7f19775]

然后为了定位错误,我们需要加上-g参数重新编译一个带调试信息的版本
widebright@widebright:~/桌面$ gcc -g main.c
widebright@widebright:~/桌面$ ./a.out
widebright received SIGSEGV! Stack trace:
0 ./a.out [0x8048580]
1 [0xb7fb3400]
2 ./a.out [0x8048636]
3 ./a.out [0x8048649]
4 ./a.out [0x804865c]
5 ./a.out [0x80486a9]
6 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5) [0xb7e52775]
7 ./a.out [0x80484c1]

加上-rdynamic 参数的话,输出的符号更清楚一些,不过好像地址不一样了。
widebright@widebright:~/桌面$ gcc -g -rdynamic main.c
widebright@widebright:~/桌面$ ./a.out
widebright received SIGSEGV! Stack trace:
0 ./a.out [0x8048840]
1 [0xb7f3d400]
2 ./a.out(error_2+0x11) [0x80488f6]
3 ./a.out(error_1+0x11) [0x8048909]
4 ./a.out(error_0+0x11) [0x804891c]
5 ./a.out(main+0x4b) [0x8048969]
6 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5) [0xb7ddc775]
7 ./a.out [0x8048781]


可以看到有调试信息的时候,错误是一样的。然后就可以用gdb定位和调试错误了:
-----------------------
(gdb) info line *0x8048580
Line 19 of "main.c" starts at address 0x804856d
  and ends at 0x8048583 .
(gdb) list *0x8048580
0x8048580 is in WidebrightSegvHandler (main.c:19).
14        char **strings;
15        size_t i, j;
16   
17        signal(signum, SIG_DFL);
18   
19        size = backtrace (array, 10);
20        strings = (char **)backtrace_symbols (array, size);
21   
22        fprintf(stderr, "widebright received SIGSEGV! Stack trace:\n");
23        for (i = 0; i < size; i++) {
-----------------
(gdb) list *0x8048636
0x8048636 is in error_2 (main.c:41).
36   
37   
38    void error_2(char * p)
39    {
40        invalide_pointer_error(p);
41    }
42   
43    void error_1(char * p)
44    {
45        error_2(p);
--------------
(gdb) list *0x8048649
0x8048649 is in error_1 (main.c:46).
41    }
42   
43    void error_1(char * p)
44    {
45        error_2(p);
46    }
47   
48    void error_0(char * p)
49    {
50        error_1(p);

=============
(gdb) br main.c:40
Breakpoint 1 at 0x804862b: file main.c, line 40.
(gdb) run
Starting program: /home/widebright/桌面/a.out

Breakpoint 1, error_2 (p=0x0) at main.c:40
40        invalide_pointer_error(p);
(gdb) stepi
0x0804862e    40        invalide_pointer_error(p);
(gdb) stepi
0x08048631    40        invalide_pointer_error(p);
(gdb) stepi
invalide_pointer_error (p=0x0) at main.c:32
32    {
(gdb) stepi
0x08048616    32    {
(gdb) stepi
33        *p = 'd'; //让这里出现一个访问非法指针的错误
(gdb) stepi
0x0804861b    33        *p = 'd'; //让这里出现一个访问非法指针的错误
(gdb) stepi

Program received signal SIGSEGV, Segmentation fault.
0x0804861b in invalide_pointer_error (p=0x0) at main.c:33
33        *p = 'd'; //让这里出现一个访问非法指针的错误

(gdb) print p
$1 = 0x0
(gdb) print *p
Cannot access memory at address 0x0




===============================================
好像使用   
  int sigaction(int signum, const struct sigaction *act,
                    struct sigaction *oldact);
http://www.kernel.org/doc/man-pages/online/pages/man2/sigaction.2.html
这个函数注册信号的处理函数的话,可以得到更多的信息,比如出错 时候的寄存器的值等等。
因为他函数 最后一个参数传过来一个ucontext_t *ucontext 的指针
可以看到 “善用backtrace解决大问题” http://blog.chinaunix.net/u/3425/showart_263408.html 这个网页上有给出一个例子。


最初看到这个用法的的在redhat的安装程序的anaconda里面的。


------------------------
关于backtrack的原理 的解释,参考这个:
从别人blog上拷来的,地址:http://blog.csdn.net/absurd/archive/2005/12/13/551585.aspx

开发嵌入式软件通常是比较麻烦的事,一些常用的工具往往无法使用,在开发PC软件时简单的任务,此时变得很复杂。今天就遇到了这样一件事,折腾了几个小时,仅仅是为知道call stack。

我编译了一个程序放到PDA(ARM9+LINUX+UCLIBC)上面运行,出现了一个ASSERT,并显示了文件名和行号,原来是调用了一个没有实现的函数,我很想知道是谁调用了它,这看似简单的问题却让我很头疼,如果有gdb,那好办-用bt命令就可以搞定,如果用的libc,那也好办-用 backtrace函数就可以搞定,问题是两者都没有。

想来想去只有自己写一个backtrace,要实现这个功能并不难,如果我们知道调用堆栈的格式,就可以很容易取出上层调用者的指令地址,有了这些上层调用者的指令地址,我们可以通过MAP文件找到指令地址对应的源文件名和行号。

下面简要介绍一下实现原理:

要获得调用者的地址,有必要介绍一下堆栈的格式:

+---------------------------+ (高地址)
+_参数1__________+
+---------------------------+
+_参数2__________+
+---------------------------+ 参数的顺序依赖于调用方式
+_参数.__________+
+---------------------------+
+_参数N__________+
+---------------------------+
+_eip____________+ 返回本次调用后,下一条指令的地址
+----------------------------+
+_ebp____________+ 这里保存的调用者的ebp
+----------------------------+
(ebp 指向这里:相当于调用者和被调用者的分界线)
+----------------------------+
+_临时变量1_______+
+----------------------------+
+_临时变量2_______+
+----------------------------+
+_临时变量.________+
+----------------------------+
+----------------------------+
+_临时变量N_______+
+----------------------------+(低地址)
由于优化、调用方式、编译器的不同,上述布局部可能有所不同,但一般来说,第一个局部变量前是调用者的ebp,ebp前是返回后下一条指令的地址。

知道了这个结构,要获得上层调用的者指令地址就容易了,我们可以用如下代码模拟glibc提供的backtrace的功能:
int backtrace (void **BUFFER, int SIZE)
{
int n = 0;
int *p = &n;
int i = 0;

int ebp = p[1];
int eip = p[2];

for(i = 0; i < SIZE; i++)
{
BUFFER[i] = (void*)eip;
p = (int*)ebp;
ebp = p[0];
eip = p[1];
}

return SIZE;
}

附:
通过addr2line可以找到地址对应的文件名和行号,不用手动去查MAP文件了。


=======================
windows系统上面要实现同样的功能,可能要调用Debug Help Library 里面的StackWalk64 等函数。
http://msdn.microsoft.com/en-us/library/ms680650(VS.85).aspx


找到一个使用StackWalk64 的例子http://www.cppblog.com/kevinlynx/archive/2008/03/28/45628.html
这里又是一个模拟backtrace(stackwalk)函数的例子
http://www.cnblogs.com/lbq1221119/archive/2008/04/18/1159956.html


其实你可以在程序的任何地方调用backtrace和 stackwalk函数的,呵呵


//////////////////another example
//funstack.c
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include

#if defined(REG_RIP)
# define SIGSEGV_STACK_IA64
# define REGFORMAT "6lx"
#elif defined(REG_EIP)
# define SIGSEGV_STACK_X86
# define REGFORMAT "x"
#else
# define SIGSEGV_STACK_GENERIC
# define REGFORMAT "%x"
#endif

static void signal_segv(int signum, siginfo_t* info, void*ptr) {
      static const char *si_codes[3] = {"", "SEGV_MAPERR", "SEGV_ACCERR"};

      size_t i;
      ucontext_t *ucontext = (ucontext_t*)ptr;

#if defined(SIGSEGV_STACK_X86) || defined(SIGSEGV_STACK_IA64)
      int f = 0;
      Dl_info dlinfo;
      void **bp = 0;
      void *ip = 0;
#else
      void *bt[20];
      char **strings;
      size_t sz;
#endif

#if defined(SIGSEGV_STACK_X86) || defined(SIGSEGV_STACK_IA64)
# if defined(SIGSEGV_STACK_IA64)
      ip = (void*)ucontext->uc_mcontext.gregs[REG_RIP];
      bp = (void**)ucontext->uc_mcontext.gregs[REG_RBP];
# elif defined(SIGSEGV_STACK_X86)
      ip = (void*)ucontext->uc_mcontext.gregs[REG_EIP];
      bp = (void**)ucontext->uc_mcontext.gregs[REG_EBP];
# endif

      fprintf(stderr, "Stack trace:\n");
      while(bp && ip) {
              if(!dladdr(ip, &dlinfo))
                      break;

              const char *symname = dlinfo.dli_sname;

              fprintf(stderr, "% 2d: %p %s+%u (%s)\n",
                              ++f,
                              ip,
                              symname,
                              (unsigned)(ip - dlinfo.dli_saddr),
                              dlinfo.dli_fname);

              if(dlinfo.dli_sname && !strcmp(dlinfo.dli_sname, "main"))
                      break;

              ip = bp[1];
              bp = (void**)bp[0];
      }
#else
      fprintf(stderr, "Stack trace (non-dedicated):\n");
      sz = backtrace(bt, 20);
      strings = backtrace_symbols(bt, sz);

      for(i = 0; i < sz; ++i)
              fprintf(stderr, "%s\n", strings[i]);
#endif
      fprintf(stderr, "End of stack trace\n");
      return;
}
int setup_sigsegv() {
      struct sigaction action;
      memset(&action, 0, sizeof(action));
      action.sa_sigaction = signal_segv;
      action.sa_flags = SA_SIGINFO;
      if(sigaction(SIGUSR1, &action, NULL) < 0) {
              perror("sigaction");
              return 0;
      }

      return 1;
}



void func1()
{
      raise(SIGUSR1);
      return ;

}
void func2()
{
      raise(SIGUSR1);
      return ;

}

void entry()
{
      func1();
      func2();
      return;
}
int main()
{
      setup_sigsegv();
      entry();
}

//////////
这问题C版ms讨论了很多次了,info gcc
__builtin_return_address
__builtin_frame_address
还有一个相关的, info libc
glibc中,
backtrace
backtrace_symbols

///////////////////////

在C/C++程序中打印当前函数调用栈


前几天帮同事跟踪的一个程序莫名退出,没有core dump(当然ulimit是打开的)的问题。我们知道,正常情况下,如果程序因为某种异常条件退出的话,应该会产生core dump,而如果程序正常退出的话,应该是直接或者间接的调用了exit()相关的函数。基于这个事实,我想到了这样一个办法,在程序开始时,通过系统提供的atexit(),向系统注册一个回调函数,在程序调用exit()退出的时候,这个回调函数就会被调用,然后我们在回调函数中打印出当前的函数调用栈,由此便可以知道exit()是在哪里调用,从而上述问题便迎刃而解了。上述方法用来解决类似问题是非常行之有效的。在上面,我提到了在“回调函数中打印出当前的函数调用栈”,相信细心的朋友应该注意到这个了,本文的主要内容就是详细介绍,如何在程序中打印中当前的函数调用栈。
我之前写过一篇题目为《介绍几个关于C/C++程序调试的函数》的文章,看到这里,请读者朋友先看一下前面这篇,因为本文是以前面这篇文章为基础的。我正是用了backtrace()和backtrace_symbols()这两个函数实现的,下面是一个简单的例子,通过这个例子我们来介绍具体的方法:

#include
#include
#include
void fun1();
void fun2();
void fun3();
void print_stacktrace();
int main()
{
  fun3();
}
void fun1()
{
  printf("stackstrace begin:\n");
  print_stacktrace();
}
void fun2()
{
  fun1();
}
void fun3()
{
  fun2();
}
void print_stacktrace()
{
  int size = 16;
  void * array[16];
  int stack_num = backtrace(array, size);
  char ** stacktrace = backtrace_symbols(array, stack_num);
  for (int i = 0; i < stack_num; ++i)
  {
      printf("%s\n", stacktrace[i]);
  }
  free(stacktrace);
}

(说明:下面的介绍采用的环境是ubuntu 11.04, x86_64, gcc-4.5.2)
1. 通过下面的方式编译运行:

wuzesheng@ubuntu:~/work/test$ gcc test.cc -o test1
wuzesheng@ubuntu:~/work/test$ ./test1
stackstrace begin:
./test1() [0x400645]
./test1() [0x400607]
./test1() [0x400612]
./test1() [0x40061d]
./test1() [0x4005ed]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xff) [0x7f5c59a91eff]
./test1() [0x400529]
    从上面的运行结果中,我们的确看到了函数的调用栈,但是都是16进制的地址,会有点小小的不爽。当然我们可以通过反汇编得到每个地址对应的函数,但这个还是有点麻烦了。不急,且听我慢慢道来,看第2步。

2. 通过下面的方式编译运行:

wuzesheng@ubuntu:~/work/test$ gcc test.cc -rdynamic -o test2
wuzesheng@ubuntu:~/work/test$ ./test2
stackstrace begin:
./test2(_Z16print_stacktracev+0x26) [0x4008e5]
./test2(_Z4fun1v+0x13) [0x4008a7]
./test2(_Z4fun2v+0x9) [0x4008b2]
./test2(_Z4fun3v+0x9) [0x4008bd]
./test2(main+0x9) [0x40088d]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xff) [0x7f9370186eff]
./test2() [0x4007c9]
    这下终于可以看到函数的名字了,对比一下2和1的编译过程,2比1多了一个-rdynamic的选项,让我们来看看这个选项是干什么的(来自gcc mannual的说明):

-rdynamic
          Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of "dlopen" or to allow obtaining backtraces from within a program.
    从上面的说明可以看出,它的主要作用是让链接器把所有的符号都加入到动态符号表中,这下明白了吧。不过这里还有一个问题,这里的函数名都是mangle过的,需要demangle才能看到原始的函数。关于c++的mangle/demangle机制,不了解的朋友可以在搜索引擎上搜一下,我这里就不多就介绍了。这里介绍如何用命令来demangle,通过c++filt命令便可以:

wuzesheng@ubuntu:~/work/test$ c++filt < << "_Z16print_stacktracev"
print_stacktrace()
    写到这里,大部分工作就ok了。不过不知道大家有没有想过这样一个问题,同一个函数可以在代码中多个地方调用,如果我们只是知道函数,而不知道在哪里调用的,有时候还是不够方便,bingo,这个也是有办法的,可以通过address2line命令来完成,我们用第2步中编译出来的test2来做实验(address2line的-f选项可以打出函数名, -C选项也可以demangle):

wuzesheng@ubuntu:~/work/test$ addr2line -a 0x4008a7 -e test2 -f
0x00000000004008a7
_Z4fun1v
??:0
    Oh no,怎么打出来的位置信息是乱码呢?不急,且看我们的第3步。

3. 通过下面的方式编译运行:

wuzesheng@ubuntu:~/work/test$ gcc test.cc -rdynamic -g -o test3
wuzesheng@ubuntu:~/work/test$ ./test3
stackstrace begin:
./test3(_Z16print_stacktracev+0x26) [0x4008e5]
./test3(_Z4fun1v+0x13) [0x4008a7]
./test3(_Z4fun2v+0x9) [0x4008b2]
./test3(_Z4fun3v+0x9) [0x4008bd]
./test3(main+0x9) [0x40088d]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xff) [0x7fa9558c1eff]
./test3() [0x4007c9]
wuzesheng@ubuntu:~/work/test$ addr2line -a 0x4008a7 -e test3 -f -C
0x00000000004008a7
fun1()
/home/wuzesheng/work/test/test.cc:20
    看上面的结果,我们不仅得到了调用栈,而且可以得到每个函数的名字,以及被调用的位置,大功告成。在这里需要说明一下的是,第3步比第2步多了一个-g选项,-g选项的主要作用是生成调试信息,位置信息就属于调试信息的范畴,经常用gdb的朋友相信不会对这个选项感到陌生。

///////////////////////////////////////////////////

介绍几个关于C/C++程序调试的函数


最近调试程序学到的几个挺有用的函数,分享一下,希望对用C/C++的朋友有所帮助!

1. 调用栈系列
下面是函数原型:

 1 2 3 4
 #include "execinfo .h" int backtrace(void **buffer, int size); char **backtrace_symbols(void *const *buffer, int size); void backtrace_symbols_fd(void *const *buffer, int size, int fd); 

接下来,对上面三个函数进行介绍:
(1)backtrace用来获得当前程序的调用栈,把结果存在buffer中。通常,我们用gdb调试程序,设置合适的断点,停下来之后,用backtrace(bt)命令,就可以看到当前的调用栈。但是,有的时候,用到条件断点的时候,gdb的功能就没有程序本身的强大了,这个时候,可以考虑在程序中调用backtrace函数,来获取调用栈。
(2)backtrace_symbols把用backtrace获取的调用栈转换成字符串数组,以字符串数组的形式返回,使用者需要在外面释放返回的字符串数组所占用的内存
(3)backtrace_symbols_fd把用backtrace获取的调用栈信息写到fd所指定的文件中

 1
 void * __builtin_return_address (unsigned int level) 

这个函数用来得到当前函数,或者调用它的函数的返回地址,得到这个地址后,通过gdb反汇编,便可得到调用函数相关的信息,这也是在应用中获取调用栈的一种方法。

2. 内存分配、释放系列

 1 2
 #include "malloc .h" size_t malloc_usable_size((void *__ptr)); 

这个函数的用法是返回调用malloc后实际分配的可用内存的大小。我们都知道在C++中,operator new()可以重载各种各样的版本,可以传入调用时的相关信息来跟踪内存分配情况,但是operator delete()却只有两种形式,不能随意重载,尤其是全局的operator delete(),只有一种版本,这个时候就比较痛苦了,究竟释放了多少内存呢? 这时候malloc_usable_size()这个函数就有用武之地了,调用它便可以获取当前释放的内存的大小。这里需要注意的是,如果在delete中用了malloc_usable_size来计算释放的内存大小,那么在new中也请用它来统计开辟的内存,这样才能对应起来。因为在调用malloc时,很多时候实际分配的内存会比用户申请的要大一些,所以如果两边的统计方法对应不起来的话,统计结果也会有比较大的判别。
这里关于new/delete重载的一些细节我不做说明,之前我写过一篇文章的,不明白的朋友可以去看一下这篇文章《细说C++中的new与delete》。

你可能感兴趣的:(Linux调用backtrack函数打印程序崩溃时的调用堆栈)