13

当我的 c++ Linux 应用程序崩溃时,我需要转储堆栈跟踪。backtrace()我使用and成功地做到了这一点backtrace_symbols()。现在,另外我想获取崩溃的行号。它是怎么做到的?

4

5 回答 5

18

我从

http://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/063/6391/6391l2.htmlhttp://www.linuxjournal.com/article/6391?page=0,0来示例代码显示了如何实现这一点。

基本上,它是将堆栈回溯放在信号处理程序中,并让后者捕获您的程序可以接收的所有“坏”信号(SIGSEGV、SIGBUS、SIGILL、SIGFPE 等)。这样,如果您的程序不幸崩溃并且您没有使用调试器运行它,您可以获得堆栈跟踪并知道故障发生在哪里。这种技术也可以用来了解你的程序在哪里循环,以防它停止响应......

下面的代码为跟踪中的每个地址运行外部程序addr2line以将其转换为文件名和行号。

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

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

    char syscom[256];
    sprintf(syscom,"addr2line %p -e sighandler", trace[i]); //last parameter is the name of this app
    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
于 2013-02-28T07:39:51.793 回答
3

gcc -Wall -g只有使用调试信息(即 with或 with )编译程序时才有可能g++ -Wall -g。没有-g可执行文件不包含任何源代码行信息。如果使用gcc,您可以使用优化和调试信息(例如g++ -Wall -g -O2)进行编译,但有时行位置会“令人惊讶”。

-Wall标志要求 GCC 显示所有警告。它非常有用(因此我建议使用它)但与-g调试信息无关。

至于如何提取行号,最简单的方法是fork一个gdb进程。或者,您可以获取调试信息(DWARF格式)并对其进行解析,可能使用libdwarfELF工具链。我不确定这是否值得麻烦...

要获得回溯,您可以简单地运行您的程序,gdb也许是gdb --args yourprogram itsarguments...


附加物

您还可以使用最近的 GCC 内部的libbacktrace(实际上是Ian Taylor 的 libbacktrace),它旨在解决您的问题(它“解释”当前可执行文件的 DWARF 格式,您将使用它进行编译g++ -O -g)。

于 2013-02-28T06:43:16.483 回答
1

正如 Saqlain 指出的,addr2line 可用于获取行号。

如果首选库,请查看LPT 套件。有关如何安装的说明在此处。LPT 依赖于 bfd 库。

于 2015-02-18T19:24:55.067 回答
1

system()需要#include <stdlib.h>

似乎缺少其他一些东西。

$ g++-8 -g -o dump dump.cpp
dump.cpp: In function ‘void bt_sighandler(int, sigcontext)’:
dump.cpp:15:43: error: ‘struct sigcontext’ has no member named
‘eip’; did > you mean ‘rip’?
         "from %p\n", sig, ctx.cr2, ctx.eip);
                                        ^~~
                                        rip
dump.cpp:21:26: error: ‘struct sigcontext’ has no member
named ‘eip’;
did you mean ‘rip’?
  trace[1] = (void *)ctx.eip;
                         ^~~
                         rip
dump.cpp: In function ‘int main()’:
dump.cpp:64:19: error: invalid conversion from ‘void*’ to
   ‘__sighandler_t’ > {aka ‘void (*)(int)’} [-fpermissive]
   sa.sa_handler = (void *)bt_sighandler;
                   ^~~~~~~~~~~~~~~~~~~~~
于 2019-03-27T23:38:29.860 回答
0

好的,所以当我被这个漂亮的笔记绊倒时,我面前打开了一个 Xavier(NVIDIA Jetpack 4.4 Ubuntu 18.04.5)。我很懊恼地发现这段代码在 Xavier 上不起作用,它基本上是一个 aarch64 架构,其中一些信号结构是不同的。

所以我设法拼凑出一个适用于 X86_64 和 aarch64 的反例。

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

#include <ucontext.h>
#include <string.h>

#ifdef __aarch64__
#define _PC pc
#define _SP sp
#elif defined(__x86_64__)
#define _PC gregs[REG_RSP]
#define _SP gregs[REG_RIP]
#else
#error architecture not supported
#endif

static void addr2line(void *traceP, void *messageP) {
  char syscom[256];
#ifdef __aarch64__
  char message[4096];
  strcpy(message, (char *) messageP);
  char *saveP = NULL, *token = strtok_r(message, "(", &saveP);
  if(token) {
    token = strtok_r(NULL, "+", &saveP);
    if(token) {
      char *term = strchr(token,')');
      if(term)
        *term = 0;
    }
  }
  if(token==NULL) {
    token = message;
  }
  sprintf(syscom,"addr2line %s -e sighandler", token);
#elif defined(__x86_64__)
  sprintf(syscom,"addr2line %p -e sighandler", traceP); //last parameter is the name of this app
#endif
  system(syscom);
}

void bt_sighandler(int sig, siginfo_t *psi, void *ctxarg) {

  void *trace[16];
  char **messages = (char **)NULL;
  int i, trace_size = 0;
  mcontext_t *ctxP = &((ucontext_t *) ctxarg)->uc_mcontext;

  if (sig == SIGSEGV)
    printf("Got signal %d, faulty address is %p, "
           "from %p\n", sig, (void *) ctxP->_PC, (void *) ctxP->_SP);
  else
    printf("Got signal %d\n", sig);

  trace_size = backtrace(trace, 16);
  /* overwrite sigaction with caller's address */
  trace[1] = (void *)ctxP->_SP;

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

    addr2line(trace[i], messages[i]);
  }

  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_sigaction = 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());
}

基本上:

  • 信号处理程序包括 SA_INFO,它的结构不同,但看起来在两种架构中都受支持。请注意,信号处理程序必须填充 sa_sigaction 而不是 sa_handler。
  • 信号处理程序“void *ctxarg”的第三个参数被转换为一个 ucontext_t,其中包括一个 mcontext_t,其中包括 PC 和 SP 字段。这些在 x86_64 和 aarch64 架构中的结构不同,因此代码必须是特定于架构的。
  • add2line 不适用于 trace[i] 中提供的地址。我还没有找到原因——这些地址以 0x55 为前缀——如果你去掉这个偏移量,它们就可以工作,但它的来源目前对我来说是个谜。但是,message[i] 包含一个可以在其位置使用的偏移量,只需要一些字符串操作即可获取它。
  • 回溯中有一个额外的条目(没有去寻找解释原因)

该代码现在在 x86_64 上运行并提供类似的可用结果。

xavier $ ./sighandler 3
Got signal 11, faulty address is 0x5563baadf4, from 0x7fd4439350
[bt] Execution path:
[bt] #1 [0x7fd4439350]
??:0
[bt] #2 ./sighandler(+0xdf4) [0x5563baadf4]
/home/jsaari/project/radar_ars/alfalfa/cuda/debug/sighandler.cpp:79
[bt] #3 ./sighandler(+0xe24) [0x5563baae24]
/home/jsaari/project/radar_ars/alfalfa/cuda/debug/sighandler.cpp:89
[bt] #4 ./sighandler(+0xea4) [0x5563baaea4]
/home/jsaari/project/radar_ars/alfalfa/cuda/debug/sighandler.cpp:109
[bt] #5 /lib/aarch64-linux-gnu/libc.so.6(__libc_start_main+0xe0) [0x7f9ae316e0]
??:0
[bt] #6 ./sighandler(+0xa94) [0x5563baaa94]
:?


x86_64 $ ./sighandler 3
Got signal 11, faulty address is 0x7ffe291d27d0, from 0x40095a
[bt] Execution path:
[bt] #1 ./sighandler() [0x40095a]
/home/jsaari/common/experiment/backtrace/sighandler.cpp:79
[bt] #2 ./sighandler() [0x40095a]
/home/jsaari/common/experiment/backtrace/sighandler.cpp:79
[bt] #3 ./sighandler() [0x400982]
/home/jsaari/common/experiment/backtrace/sighandler.cpp:89
[bt] #4 ./sighandler() [0x4009f4]
/home/jsaari/common/experiment/backtrace/sighandler.cpp:109
[bt] #5 /lib64/libc.so.6(__libc_start_main+0xf5) [0x7f294a36d495]
??:0
[bt] #6 ./sighandler() [0x4006d9]
??:?

恕我直言 - 现在 NVIDIA 已经收购了 ARM,ARM 将接管物联网(如果还没有的话),英特尔可能会开始苦苦挣扎。


显然,这里记录了一个地址加扰:

https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=860394

显然,这是由 gcc 仅构建与位置无关的代码引起的。如果您使用“-no-pie”构建应用程序,那么 trace[i] 条目将与 addr2line 一起使用。

除了我编码的过程之外,我还没有找到如何管理它。


从这个帖子:

如何找到 PIE 二进制文件的负载重定位?

重定位值可以从

#include <link.h>

. . .

uintptr_t relocation = _r_dump.r_map->l_addr;

可以从 trace[i] 中减去重定位值以获得 addr2line 可以使用的地址(我正在使用的 xavier aarch64 框的结果)。

对于非重定位二进制文​​件,重定位的值为“0”(我正在使用的 x86_64 框上的结果)。

于 2020-10-06T17:41:35.530 回答