如何使用带有行号信息的 gcc 获取 C 的堆栈跟踪?

新手上路,请多包涵

我们在专有的 assert 中使用堆栈跟踪来捕捉开发人员的错误 - 当发现错误时,会打印堆栈跟踪。

我发现 gcc 的对 backtrace() / backtrace_symbols() 方法不足:

  1. 名称被破坏
  2. 没有线路信息

第一个问题可以通过 abi::__cxa_demangle 解决。

然而,第二个问题更加棘手。我找到 了 backtrace_symbols() 的替代品。这比 gcc 的 backtrace_symbols() 更好,因为它可以检索行号(如果使用 -g 编译)并且您不需要使用 -rdynamic 编译。

悬停代码是 GNU 许可的,所以恕我直言,我不能在商业代码中使用它。

有什么建议吗?

附言

gdb 能够打印出传递给函数的参数。可能要求已经太多了:)

PS 2

类似的问题(感谢 nobar)

原文由 dimba 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.5k
2 个回答

不久前 我回答了一个类似的问题。您应该查看方法 #4 上可用的源代码,该方法还打印行号和文件名。

  • 方法#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\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

原文由 karlphillip 发布,翻译遵循 CC BY-SA 4.0 许可协议

我必须在有很多限制的生产环境中执行此操作,所以我想解释一下已经发布的方法的优缺点。

  1. 附加 GDB

+ 非常简单和强大

- 对于大型程序来说很慢,因为 GDB 坚持将 整个 地址加载到 # 行数据库中而不是懒惰地加载

- 干扰信号处理。 GDB在附加的时候,拦截了SIGINT(ctrl-c)之类的信号,会导致程序卡在GDB交互提示?如果某个其他进程定期发送此类信号。也许有一些方法可以解决它,但这使得 GDB 在我的情况下无法使用。如果您只关心在程序崩溃时打印一次调用堆栈,而不是多次打印,您仍然可以使用它。

  1. 地址 2 行。这是不使用 backtrace_symbols 的替代解决方案。

+ 不从堆中分配,这在信号处理程序中是不安全的

+ 不需要解析 backtrace_symbols 的输出

- 不适用于没有 dladdr1 的 MacOS。您可以改用 _dyld_get_image_vmaddr_slide,它返回与 link_map::l_addr 相同的偏移量。

- 需要添加负偏移量,否则翻译后的行号将大 1。 backtrace_symbols 为您执行此操作

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

// converts a function's address in memory to its VMA address in the executable file. VMA is what addr2line expects
size_t ConvertToVMA(size_t addr)
{
  Dl_info info;
  link_map* link_map;
  dladdr1((void*)addr,&info,(void**)&link_map,RTLD_DL_LINKMAP);
  return addr-link_map->l_addr;
}

void PrintCallStack()
{
  void *callstack[128];
  int frame_count = backtrace(callstack, sizeof(callstack)/sizeof(callstack[0]));
  for (int i = 0; i < frame_count; i++)
  {
    char location[1024];
    Dl_info info;
    if(dladdr(callstack[i],&info))
    {
      char command[256];
      size_t VMA_addr=ConvertToVMA((size_t)callstack[i]);
      //if(i!=crash_depth)
        VMA_addr-=1;    // https://stackoverflow.com/questions/11579509/wrong-line-numbers-from-addr2line/63841497#63841497
      snprintf(command,sizeof(command),"addr2line -e %s -Ci %zx",info.dli_fname,VMA_addr);
      system(command);
    }
  }
}

void Foo()
{
  PrintCallStack();
}

int main()
{
  Foo();
  return 0;
}

我还想澄清 backtrace 和 backtrace_symbols 生成什么地址以及 addr2line 期望什么。 在此处输入图像描述 addr2line 需要 Foo VMA ,或者如果您使用 –section=.text,则需要 Foo文件- 文本文件。回溯返回 Foo mem 。 backtrace_symbols 在某处生成 Foo VMA 。我在其他几篇文章中犯和看到的一个大错误是假设 VMA base = 0 或 Foo VMA = Foo file = Foo mem - ELF mem ,这很容易计算。这通常有效,但对于某些编译器(即链接器脚本)使用 VMA base > 0。例如 Ubuntu 16 (0x400000) 上的 GCC 5.4 和 MacOS (0x100000000) 上的 clang 11。对于共享库,它始终为 0。似乎 VMAbase 仅对非位置无关代码有意义。否则,它不会影响 EXE 在内存中的加载位置。

此外,karlphillip 和这个都不需要使用 -rdynamic 进行编译。这将增加二进制大小,特别是对于大型 C++ 程序或共享库,动态符号表中的无用条目永远不会被导入

原文由 Yale Zhang 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题