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

2024-10-21 09:13:00
admin
原创
67
摘要:问题描述:使用这样的函数:#include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> void print_trace() {...

问题描述:

使用这样的函数:

#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);
    }
}

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

还有什么其他方法可以实现?


解决方案 1:

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

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

  • 方法 1:

第一种方法是使用打印和日志消息进行传播,以便精确定位执行路径。在复杂的程序中,即使在某些 GCC 特定宏的帮助下,此选项也可以变得繁琐和乏味。例如,考虑以下调试宏:

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

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

  • 方法 #2:(它没有提到行号,但我在方法 4 中提到了)

但是,获取堆栈回溯的更好方法是使用 glibc 提供的一些特定支持函数。关键函数是 backtrace(),它将堆栈框架从调用点导航到程序开头并提供返回地址数组。然后,您可以通过使用 nm 命令查看目标文件,将每个地址映射到代码中特定函数的主体。或者,您可以使用更简单的方法 - 使用 backtrace_symbols()。此函数将 backtrace() 返回的返回地址列表转换为字符串列表,每个字符串包含函数名称在函数内的偏移量和返回地址。字符串列表是从堆空间分配的(就像您调用 malloc() 一样),因此您应该在完成后立即释放它。

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

  • 方法 #3:(方法 2 的更好实现方式)

此技术的一个更有用的应用是将堆栈回溯放入信号处理程序中,并让后者捕获程序可以接收的所有“坏”信号(SIGSEGV、SIGBUS、SIGILL、SIGFPE 等)。这样,如果您的程序不幸崩溃并且您没有使用调试器运行它,您可以获得堆栈跟踪并知道错误发生的位置。此技术还可用于了解程序在停止响应的情况下循环的位置

此技术的实现可在此处获得。

  • 方法 4:

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

基本上,我遵循了一个使用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
", 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());
}

该代码应被编译为: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中抓取了可执行文件名称,对其进行了一点改进。以下是最新版本:

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);
}

并像这样初始化:

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());

}

解决方案 2:

如果您使用的是 Linux,标准 C 库包含一个名为 的函数backtrace,该函数用框架的返回地址填充数组,还有另一个名为 的函数backtrace_symbols,该函数将从中获取地址backtrace并查找相应的函数名称。这些都记录在GNU C 库手册中。

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

解决方案 3:

nobar发表了一个很棒的答案。简而言之;

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

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

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

解决方案 4:

是不是abort()更简单呢?

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

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用