从程序内部调用 gdb 以打印其堆栈跟踪的最佳方法? [英] Best way to invoke gdb from inside program to print its stacktrace?

查看:42
本文介绍了从程序内部调用 gdb 以打印其堆栈跟踪的最佳方法?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

使用这样的函数:

#include #include #include #include 无效打印跟踪(){字符 pid_buf[30];sprintf(pid_buf, "--pid=%d", getpid());char name_buf[512];name_buf[readlink("/proc/self/exe", name_buf, 511)]=0;int child_pid = fork();如果(!child_pid){dup2(2,1);//将输出重定向到 stderrfprintf(stdout,"%s 的堆栈跟踪 pid=%s
",name_buf,pid_buf);execlp("gdb", "gdb", "--batch", "-n", "-ex", "thread", "-ex", "bt", name_buf, pid_buf, NULL);中止();/* 如果 gdb 启动失败 */} 别的 {waitpid(child_pid,NULL,0);}}

我在输出中看到了 print_trace 的详细信息.

还有哪些方法可以做到?

解决方案

您在我的另一个答案(现已删除)中提到您还想查看行号.我不知道从应用程序内部调用 gdb 时该怎么做.

但我将与您分享几种打印带有函数名称及其各自行号的简单堆栈跟踪的方法不使用 gdb.其中大部分来自Linux Journal非常好的文章:

  • 方法 1:
<块引用>

第一种方法是传播按顺序打印和记录消息以查明执行路径.在一个复杂的程序,这个选项可以变得繁琐和乏味,即使,在一些 GCC 特定的帮助下宏,它可以简化一点.例如,考虑一个调试宏例如:

 #define TRACE_MSG fprintf(stderr, __FUNCTION__ "() [%s:%d] 我在这里
", \__FILE__, __LINE__)

<块引用>

你可以快速传播这个宏在整个程序中通过剪切和粘贴它.当你不需要它时不再,只需将其关闭将其定义为无操作.

  • 方法 #2:(它没有说明行号,但我使用方法 4)
<块引用>

获得堆栈回溯的更好方法,然而,是使用一些提供的特定支持功能glibc.关键之一是 backtrace(),从中导航堆栈帧开始的调用点该程序并提供了一系列返回地址.然后你可以映射每个地址的主体代码中的特定功能查看目标文件nm 命令.或者,你可以这样做更简单的方法——使用 backtrace_symbols().这个函数转换一个列表返回地址,如返回backtrace(),进入一个字符串列表,每个包含函数名称函数内的偏移量和退货地址.字符串列表是从你的堆空间分配(好像你调用了 malloc()),所以你应该完成后立即释放()它

我鼓励您阅读它,因为该页面有 源代码 示例.为了将地址转换为函数名称,您必须使用 -rdynamic 选项编译您的应用程序.

  • 方法 3:(方法 2 的更好方法)
<块引用>

一个更有用的应用程序这种技术是把堆栈信号处理程序内的回溯和让后者抓住所有的坏"您的程序可以接收的信号(SIGSEGV、SIGBUS、SIGILL、SIGFPE 和类似).这样,如果你的程序不幸的是崩溃了,而你没有用调试器运行它,你可以获取堆栈跟踪并知道在哪里故障发生.这种技术也可用于了解您的位置程序正在循环,以防它停止回应

可以在此处获得此技术的实现.

  • 方法 4:

我对方法#3 进行了一个小的改进来打印行号.这也可以复制到方法 #2 上.

基本上,我遵循了使用addr2line

<块引用>

将地址转换为文件名并行号.

下面的源代码打印所有本地函数的行号.如果调用另一个库中的函数,您可能会看到几个 ??:0 而不是文件名.

#include #include #include #include #include void bt_sighandler(int sig, struct sigcontext ctx) {无效*跟踪[16];char **messages = (char **)NULL;int i,trace_size = 0;如果(信号 == SIGSEGV)printf("得到信号%d,错误地址为%p,""来自 %p
", sig, ctx.cr2, ctx.eip);别的printf("得到信号%d
", sig);trace_size = backtrace(trace, 16);/* 用调用者的地址覆盖 sigaction */trace[1] = (void *)ctx.eip;消息 = backtrace_symbols(trace, trace_size);/* 跳过第一个栈帧(指向这里)*/printf("[bt] 执行路径:
");for (i=1; i

这段代码应该编译为:gcc sighandler.c -o sighandler -rdynamic

程序输出:

得到信号11,错误地址是0xdeadbeef,来自0x8048975[bt] 执行路径:[bt] #1 ./sighandler(func_a+0x1d) [0x8048975]/home/karl/workspace/stacktrace/sighandler.c:44[bt] #2 ./sighandler(func_b+0x20) [0x804899f]/home/karl/workspace/stacktrace/sighandler.c:54[bt] #3 ./sighandler(main+0x6c) [0x8048a16]/home/karl/workspace/stacktrace/sighandler.c:74[bt] #4/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0x3fdbd6]??:0[bt] #5 ./sighandler() [0x8048781]??:0

<小时>

更新 2012/04/28 对于最近的 linux 内核版本,上述 sigaction 签名已过时.我还通过从 this answer 中获取可执行文件名称对其进行了一些改进.这是一个最新版本:

char* exe = 0;int initialiseExecutableName(){字符链接[1024];exe = 新字符[1024];snprintf(link,sizeof link,"/proc/%d/exe",getpid());如果(读取链接(链接,exe,链接大小)==-1){fprintf(stderr,"ERRORRRRR
");退出(1);}printf("初始化的可执行文件名:%s
",exe);}const char* getExecutableName(){如果(exe == 0)初始化ExecutableName();返回exe;}/* 从 ucontext.h 获取 REG_EIP */#define __USE_GNU#include void bt_sighandler(int sig, siginfo_t *info,无效*秘密){无效*跟踪[16];char **messages = (char **)NULL;int i,trace_size = 0;ucontext_t *uc = (ucontext_t *)secret;/* 用 siginfo_t 做一些有用的事情 */如果(信号 == SIGSEGV)printf("得到信号%d,错误地址为%p,"来自 %p
",信号,信息-> si_addr,uc->uc_mcontext.gregs[REG_EIP]);别的printf("得到信号%d
", sig);trace_size = backtrace(trace, 16);/* 用调用者的地址覆盖 sigaction */trace[1] = (void *) uc->uc_mcontext.gregs[REG_EIP];消息 = backtrace_symbols(trace, trace_size);/* 跳过第一个栈帧(指向这里)*/printf("[bt] 执行路径:
");for (i=1; i

并像这样初始化:

int main() {/* 安装我们的信号处理程序 */struct sigaction sa;sa.sa_sigaction = (void *)bt_sighandler;sigemptyset (&sa.sa_mask);sa.sa_flags = SA_RESTART |SA_SIGINFO;sigaction(SIGSEGV, &sa, NULL);sigaction(SIGUSR1, &sa, NULL);/* ... 在此处添加任何其他信号 *//* 做点什么 */printf("%d
", func_b());}

Using a function like this:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

void print_trace() {
    char pid_buf[30];
    sprintf(pid_buf, "--pid=%d", getpid());
    char name_buf[512];
    name_buf[readlink("/proc/self/exe", name_buf, 511)]=0;
    int child_pid = fork();
    if (!child_pid) {           
        dup2(2,1); // redirect output to stderr
        fprintf(stdout,"stack trace for %s pid=%s
",name_buf,pid_buf);
        execlp("gdb", "gdb", "--batch", "-n", "-ex", "thread", "-ex", "bt", name_buf, pid_buf, NULL);
        abort(); /* If gdb failed to start */
    } else {
        waitpid(child_pid,NULL,0);
    }
}

I see the details of print_trace in the output.

What are other ways to do it?

解决方案

You mentioned on my other answer (now deleted) that you also want to see line numbers. I'm not sure how to do that when invoking gdb from inside your application.

But I'm going to share with you a couple of ways to print a simple stacktrace with function names and their respective line numbers without using gdb. Most of them came from a very nice article from Linux Journal:

  • Method #1:

The first method is to disseminate it with print and log messages in order to pinpoint the execution path. In a complex program, this option can become cumbersome and tedious even if, with the help of some GCC-specific macros, it can be simplified a bit. Consider, for example, a debug macro such as:

 #define TRACE_MSG fprintf(stderr, __FUNCTION__     
                          "() [%s:%d] here I am
", 
                          __FILE__, __LINE__)

You can propagate this macro quickly throughout your program by cutting and pasting it. When you do not need it anymore, switch it off simply by defining it to no-op.

  • Method #2: (It doesn't say anything about line numbers, but I do on method 4)

A nicer way to get a stack backtrace, however, is to use some of the specific support functions provided by glibc. The key one is backtrace(), which navigates the stack frames from the calling point to the beginning of the program and provides an array of return addresses. You then can map each address to the body of a particular function in your code by having a look at the object file with the nm command. Or, you can do it a simpler way--use backtrace_symbols(). This function transforms a list of return addresses, as returned by backtrace(), into a list of strings, each containing the function name offset within the function and the return address. The list of strings is allocated from your heap space (as if you called malloc()), so you should free() it as soon as you are done with it.

I encourage you to read it since the page has source code examples. In order to convert an address to a function name you must compile your application with the -rdynamic option.

  • Method #3: (A better way of doing method 2)

An even more useful application for this technique is putting a stack backtrace inside a signal handler and having the latter catch all the "bad" signals your program can receive (SIGSEGV, SIGBUS, SIGILL, SIGFPE and the like). This way, if your program unfortunately crashes and you were not running it with a debugger, you can get a stack trace and know where the fault happened. This technique also can be used to understand where your program is looping in case it stops responding

An implementation of this technique is available here.

  • Method #4:

A small improvement I've done on method #3 to print line numbers. This could be copied to work on method #2 also.

Basically, I followed a tip that uses addr2line to

convert addresses into file names and line numbers.

The source code below prints line numbers for all local functions. If a function from another library is called, you might see a couple of ??:0 instead of file names.

#include <stdio.h>
#include <signal.h>
#include <stdio.h>
#include <signal.h>
#include <execinfo.h>

void bt_sighandler(int sig, struct sigcontext ctx) {

  void *trace[16];
  char **messages = (char **)NULL;
  int i, trace_size = 0;

  if (sig == SIGSEGV)
    printf("Got signal %d, faulty address is %p, "
           "from %p
", sig, ctx.cr2, ctx.eip);
  else
    printf("Got signal %d
", sig);

  trace_size = backtrace(trace, 16);
  /* overwrite sigaction with caller's address */
  trace[1] = (void *)ctx.eip;
  messages = backtrace_symbols(trace, trace_size);
  /* skip first stack frame (points here) */
  printf("[bt] Execution path:
");
  for (i=1; i<trace_size; ++i)
  {
    printf("[bt] #%d %s
", i, messages[i]);

    /* find first occurence of '(' or ' ' in message[i] and assume
     * everything before that is the file name. (Don't go beyond 0 though
     * (string terminator)*/
    size_t p = 0;
    while(messages[i][p] != '(' && messages[i][p] != ' '
            && messages[i][p] != 0)
        ++p;

    char syscom[256];
    sprintf(syscom,"addr2line %p -e %.*s", trace[i], p, messages[i]);
        //last parameter is the file name of the symbol
    system(syscom);
  }

  exit(0);
}


int func_a(int a, char b) {

  char *p = (char *)0xdeadbeef;

  a = a + b;
  *p = 10;  /* CRASH here!! */

  return 2*a;
}


int func_b() {

  int res, a = 5;

  res = 5 + func_a(a, 't');

  return res;
}


int main() {

  /* Install our signal handler */
  struct sigaction sa;

  sa.sa_handler = (void *)bt_sighandler;
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = SA_RESTART;

  sigaction(SIGSEGV, &sa, NULL);
  sigaction(SIGUSR1, &sa, NULL);
  /* ... add any other signal here */

  /* Do something */
  printf("%d
", func_b());
}

This code should be compiled as: gcc sighandler.c -o sighandler -rdynamic

The program outputs:

Got signal 11, faulty address is 0xdeadbeef, from 0x8048975
[bt] Execution path:
[bt] #1 ./sighandler(func_a+0x1d) [0x8048975]
/home/karl/workspace/stacktrace/sighandler.c:44
[bt] #2 ./sighandler(func_b+0x20) [0x804899f]
/home/karl/workspace/stacktrace/sighandler.c:54
[bt] #3 ./sighandler(main+0x6c) [0x8048a16]
/home/karl/workspace/stacktrace/sighandler.c:74
[bt] #4 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0x3fdbd6]
??:0
[bt] #5 ./sighandler() [0x8048781]
??:0


Update 2012/04/28 for recent linux kernel versions, the above sigaction signature is obsolete. Also I improved it a bit by grabbing the executable name from this answer. Here is an up to date version:

char* exe = 0;

int initialiseExecutableName() 
{
    char link[1024];
    exe = new char[1024];
    snprintf(link,sizeof link,"/proc/%d/exe",getpid());
    if(readlink(link,exe,sizeof link)==-1) {
        fprintf(stderr,"ERRORRRRR
");
        exit(1);
    }
    printf("Executable name initialised: %s
",exe);
}

const char* getExecutableName()
{
    if (exe == 0)
        initialiseExecutableName();
    return exe;
}

/* get REG_EIP from ucontext.h */
#define __USE_GNU
#include <ucontext.h>

void bt_sighandler(int sig, siginfo_t *info,
                   void *secret) {

  void *trace[16];
  char **messages = (char **)NULL;
  int i, trace_size = 0;
  ucontext_t *uc = (ucontext_t *)secret;

  /* Do something useful with siginfo_t */
  if (sig == SIGSEGV)
    printf("Got signal %d, faulty address is %p, "
           "from %p
", sig, info->si_addr, 
           uc->uc_mcontext.gregs[REG_EIP]);
  else
    printf("Got signal %d
", sig);

  trace_size = backtrace(trace, 16);
  /* overwrite sigaction with caller's address */
  trace[1] = (void *) uc->uc_mcontext.gregs[REG_EIP];

  messages = backtrace_symbols(trace, trace_size);
  /* skip first stack frame (points here) */
  printf("[bt] Execution path:
");
  for (i=1; i<trace_size; ++i)
  {
    printf("[bt] %s
", messages[i]);

    /* find first occurence of '(' or ' ' in message[i] and assume
     * everything before that is the file name. (Don't go beyond 0 though
     * (string terminator)*/
    size_t p = 0;
    while(messages[i][p] != '(' && messages[i][p] != ' '
            && messages[i][p] != 0)
        ++p;

    char syscom[256];
    sprintf(syscom,"addr2line %p -e %.*s", trace[i] , p, messages[i] );
           //last parameter is the filename of the symbol
    system(syscom);

  }
  exit(0);
}

and initialise like this:

int main() {

  /* Install our signal handler */
  struct sigaction sa;

  sa.sa_sigaction = (void *)bt_sighandler;
  sigemptyset (&sa.sa_mask);
  sa.sa_flags = SA_RESTART | SA_SIGINFO;

  sigaction(SIGSEGV, &sa, NULL);
  sigaction(SIGUSR1, &sa, NULL);
  /* ... add any other signal here */

  /* Do something */
  printf("%d
", func_b());

}

这篇关于从程序内部调用 gdb 以打印其堆栈跟踪的最佳方法?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆