从程序内部调用 gdb 以打印其堆栈跟踪的最佳方法?

Posted

技术标签:

【中文标题】从程序内部调用 gdb 以打印其堆栈跟踪的最佳方法?【英文标题】:Best way to invoke gdb from inside program to print its stacktrace? 【发布时间】:2011-03-10 06:28:01 【问题描述】:

使用这样的函数:

#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\n",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);
    

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

还有什么其他方法可以做到这一点?

【问题讨论】:

有问题吗?它不做的事情? @Adam Shiemke 列出了一些问题。可能是 gdb 可以以更适当的方式调用。可能是我需要一些特别的东西来支持多线程。可能有办法使它可移植,或者有特殊的“libstacktrace.so”。讨论。 您可以多次使用 -ex 选项。 @Derek Ledbetter,好的,正在申请。 附带说明,有一个用于附加 GDB 的包装库:libdebugme。 【参考方案1】:

您在我的另一个答案(现已删除)中提到您还想查看行号。从应用程序内部调用 gdb 时,我不确定如何执行此操作。

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

方法#1:

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

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

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

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

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

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

方法 #3:(方法 2 的更好方法)

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

here 提供了此技术的实现。

方法#4:

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

基本上,我followed a tip 使用 addr2line

将地址转换为文件名和 行号。

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

#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\n", sig, ctx.cr2, ctx.eip);
  else
    printf("Got signal %d\n", 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:\n");
  for (i=1; i<trace_size; ++i)
  
    printf("[bt] #%d %s\n", 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\n", func_b());

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

程序输出:

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

更新 2012/04/28 对于最近的 linux 内核版本,上面的 sigaction 签名已过时。我还通过从this answer 获取可执行文件名称对其进行了一些改进。这是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\n");
        exit(1);
    
    printf("Executable name initialised: %s\n",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\n", sig, info->si_addr, 
           uc->uc_mcontext.gregs[REG_EIP]);
  else
    printf("Got signal %d\n", 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:\n");
  for (i=1; i<trace_size; ++i)
  
    printf("[bt] %s\n", 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);

并像这样初始化:

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\n", func_b());


【讨论】:

"Method #1" ->关于如何自动“传播”它还有我的另一个问题,但没有有用的答案。 方法 #2 - #4 -> 已经尝试过 - 它有效:vi-server.org/vi/simple_sampling_profiler.html 但是 backtrace/addr2line 方法有局限性:1. addr2line 经常无法找出行(而 gdb 可以),2。 gdb 可以迭代线程:“thread apply all bt”。 @Vi 这个人成功了:***.com/questions/4636456/stack-trace-for-c-using-gcc/… @karlphillip:我找到了另一种将文件和行号放入堆栈跟踪的方法。像他们在 refdbg 中那样使用 libbfd (sourceware.org/binutils/docs-2.21/bfd/…):refdbg.cvs.sourceforge.net/viewvc/refdbg/refdbg/… 我自己还没有尝试过。 除了使用-rdynamic,还要检查你的构建系统没有添加-fvisibility=hidden选项! (因为它会完全丢弃-rdynamic的效果)【参考方案2】:

如果您使用的是 Linux,标准 C 库包括一个名为 backtrace 的函数,它使用帧的返回地址填充一个数组,以及另一个名为 backtrace_symbols 的函数,它将从 backtrace 和查找相应的函数名称。这些记录在GNU C Library manual 中。

那些不会显示参数值、源代码行等,它们只适用于调用线程。但是,它们应该比以这种方式运行 GDB 快得多(并且可能不那么不稳定),所以它们有自己的位置。

【讨论】:

实际上我插入程序的 sn-p 首先输出带有 backtrace_symbols 的回溯,然后启动 gdb 以输出所有线程的完全注释的堆栈跟踪。如果 gdb 失败,我仍然有 backtrace 的堆栈跟踪。【参考方案3】:

nobar 已发布a fantastic answer。总之;

因此,您需要一个打印堆栈跟踪的独立函数,该函数具有 gdb 堆栈跟踪所具有的所有功能,并且不会终止您的应用程序。答案是以非交互模式自动启动 gdb 以执行您想要的任务。

这是通过在子进程中执行 gdb、使用 fork() 并编写脚本以在应用程序等待它完成时显示堆栈跟踪来完成的。这可以在不使用核心转储且不中止应用程序的情况下执行。

我相信这就是你要找的东西,@Vi

【讨论】:

看问题中的示例代码。就是那个方法。我正在寻找其他不那么重量级的方法。 addr2line-quality 东西的主要问题是它经常无法显示 gdb 可以显示的行号。 @Vi 在他的回答中说他从这个线程中的问题中得到了基本代码。但是,如果您仔细观察,您会发现存在一些差异。你试过了吗?【参考方案4】:

abort() 不是更简单吗?

这样,如果它发生在现场,客户可以将核心文件发送给您(我不知道有多少用户参与到 my 应用程序中,希望我强迫他们调试它)。

【讨论】:

我不需要中止。我需要一个堆栈跟踪。打印后程序可以继续。我喜欢“bt full”的冗长 另外 print_trace() 方式是相当不具侵入性的。如果gdb in not found 程序可以继续而不打印堆栈跟踪。 @Vi,好的,对不起,我没有任何帮助:o/

以上是关于从程序内部调用 gdb 以打印其堆栈跟踪的最佳方法?的主要内容,如果未能解决你的问题,请参考以下文章

从核心转储中获取堆栈跟踪

如何获取 gdb 调用堆栈跟踪?

使用 GDB 在堆栈上打印符号

跟踪堆栈外部库 eclipse

堆栈跟踪中没有函数名称,GDB,但出现在LLDB中[重复]

GDB 获取python程序堆栈