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

73

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

我认为gcc的backtrace()/backtrace_symbols()方法不够用:

  1. 名称被混淆了
  2. 没有行信息

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

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

但是代码使用GNU许可证,所以我认为不能在商业代码中使用。

有什么建议吗?

P.S.

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

PS 2

类似的问题(感谢nobar)


4
要么找到作者并付款,要么自己重新实现它。 - the_drow
1
我不确定在商业应用程序中使用编译的GNU代码是否与修改/定制GNU代码本身以在您的应用程序内分发相同。有人知道吗? - karlphillip
1
这段代码是仅适用于Linux/x86平台,还是可以在其他平台上运行? - osgx
返回转换的文本内容:没有行号要求:https://dev59.com/Wm865IYBdhLWcg3wR8ah - Ciro Santilli OurBigBook.com
15个回答

48

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

通过使用fork()在子进程中执行gdb并对其进行脚本编写以显示堆栈跟踪,从而完成此操作,同时应用程序等待它完成。可以在不使用核心转储和不中止应用程序的情况下执行此操作。我是通过查看这个问题来学习如何做到这一点的:如何更好地调用程序以打印其堆栈跟踪?

该问题发布的示例对我来说并没有完全按照原样工作,因此这是我“修复”的版本(我在Ubuntu 9.04上运行了此版本)。

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

void print_trace() {
    char pid_buf[30];
    sprintf(pid_buf, "%d", getpid());
    char name_buf[512];
    name_buf[readlink("/proc/self/exe", name_buf, 511)]=0;
    prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);
    int child_pid = fork();
    if (!child_pid) {
        dup2(2,1); // redirect output to stderr - edit: unnecessary?
        execl("/usr/bin/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);
    }
}

正如引用的问题所示,gdb提供了一些其他选项可供使用。例如,使用"bt full"而不是"bt"将产生更详细的报告(输出中包括局部变量)。 gdb的man页面有点简单,但完整的文档在这里提供。

由于它基于gdb,因此输出包括解码后的名称行号函数参数,甚至可选的局部变量。此外,gdb具有线程感知功能,因此您应该能够提取一些特定于线程的元数据。

以下是我使用此方法看到的堆栈跟踪类型的示例。

0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6
[Current thread is 0 (process 15573)]
#0  0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6
#1  0x0000000000400bd5 in print_trace () at ./demo3b.cpp:496
2  0x0000000000400c09 in recursive (i=2) at ./demo3b.cpp:636
3  0x0000000000400c1a in recursive (i=1) at ./demo3b.cpp:646
4  0x0000000000400c1a in recursive (i=0) at ./demo3b.cpp:646
5  0x0000000000400c46 in main (argc=1, argv=0x7fffe3b2b5b8) at ./demo3b.cpp:70

注意:我发现这个方法与使用valgrind不兼容(可能是由于Valgrind使用的虚拟机)。当您在gdb会话中运行程序时,它也无法工作(无法对进程应用第二个“ptrace”实例)。


5
不要使用这个!我在我的程序中直接使用了上面的函数,在Ubuntu 12.04上它会完全崩溃X服务器。 - BeniBela
2
@BeniBela,你运行的是什么类型的程序?是一些低级别的东西吗?这种方法在Fedora 17中对我很有效。 - Joshua Chia
6
情况正在恶化:ptrace跟踪父进程现在不再允许。但是也许你可以使用prctl设置一个标志吗? - BeniBela
1
@BeniBela:感谢您的指引。一个可能的解决方法是使用“sudo”运行。 - Brent Bradburn
5
fork() 前使用 #include <sys/prctl.h>prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0); 可以绕过它。 - Giovanni Funchal
显示剩余11条评论

37

不久前我回答了类似的问题。您应该查看第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

2
请记得使用 -rdynamic 编译您的应用程序。 - karlphillip
1
@karlphillip,关于GPL。如果GPL(而不是GNU,但是受GPL许可的)代码与另一个代码链接(使用ld.so或ld),则GPL要求另一个代码在GPL下可用。只有在将应用程序转移到其他人时才会出现这种情况。个人可以对GPL代码做任何事情并将其与任何东西链接。 - osgx
1
@dimba:不要忘记给他颁发奖赏(就在接受选项下面!!) - Goz
@karlphillip 我尝试了你的解决方案并查看了Linux Journal文章,但是我需要在程序不崩溃的情况下执行该程序中所做的操作。基本上,我正在尝试实现一个自定义异常类,当捕获到异常时,它将打印回溯和行号等信息。因此,我没有SIGSEGV出现的情况。有什么想法如何实现这一点吗? - Chani
1
错误:‘struct sigcontext’没有名为‘eip’的成员;您是否想说‘rip’? - neoexpert
显示剩余7条评论

11

这个问题在How to generate a stacktrace when my gcc C++ app crashes中得到了广泛的讨论。提供了很多建议,包括如何在运行时生成堆栈跟踪的讨论。

我最喜欢的回答来自那个线程中的个人收藏,建议启用核心转储(core dumps),它允许你在崩溃时查看完整的应用程序状态(包括函数参数、行号和非重载名称)。此方法的另一个好处是不仅适用于assertions,还适用于分段错误(segmentation faults)未经处理的异常(unhandled exceptions)

不同的Linux shell使用不同的命令来启用核心转储,但你可以在应用程序代码中使用类似以下内容来实现...

#include <sys/resource.h>
...
struct rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };
assert( setrlimit( RLIMIT_CORE, &core_limit ) == 0 ); // enable core dumps for debug builds

在程序崩溃后,运行您喜欢的调试器以检查程序状态。

$ kdbg executable core

这是一些示例输出...

alt text

也可以在命令行从核心转储中提取堆栈跟踪。

$ ( CMDFILE=$(mktemp); echo "bt" >${CMDFILE}; gdb 2>/dev/null --batch -x ${CMDFILE} temp.exe core )
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 22857]
#0  0x00007f4189be5fb5 in raise () from /lib/libc.so.6
#0  0x00007f4189be5fb5 in raise () from /lib/libc.so.6
#1  0x00007f4189be7bc3 in abort () from /lib/libc.so.6
#2  0x00007f4189bdef09 in __assert_fail () from /lib/libc.so.6
#3  0x00000000004007e8 in recursive (i=5) at ./demo1.cpp:18
#4  0x00000000004007f3 in recursive (i=4) at ./demo1.cpp:19
#5  0x00000000004007f3 in recursive (i=3) at ./demo1.cpp:19
#6  0x00000000004007f3 in recursive (i=2) at ./demo1.cpp:19
#7  0x00000000004007f3 in recursive (i=1) at ./demo1.cpp:19
#8  0x00000000004007f3 in recursive (i=0) at ./demo1.cpp:19
#9  0x0000000000400849 in main (argc=1, argv=0x7fff2483bd98) at ./demo1.cpp:26

2
gdb 用于后期分析。我更想知道如何从代码内部获取信息。也许我想打印回溯,而不是在 SIGSEV 的情况下 - 例如查看未处理的 C++ 异常是从哪里抛出的。 - dimba
1
未处理的异常将生成一个核心转储文件,您可以使用它来分析抛出时的堆栈 - 因此,这个答案适用于这种情况。 - Brent Bradburn
另一方面,如果您想在不终止程序的情况下生成堆栈跟踪,我发布了另一个回答来解决这个要求。 - Brent Bradburn
如果二进制文件是从开源代码编译而来的外部系统,那么在你的系统上无法理解核心转储文件。同时,你也不能要求用户运行这样的命令。 - xryl669

6

由于GPL许可的代码旨在帮助您进行开发,因此您可以选择不将其包含在最终产品中。GPL限制您在与不兼容GPL的代码链接后分发GPL许可证代码。只要您仅在公司内部使用GPL代码,就应该没有问题。


动态链接可能(几乎肯定)没问题,因为源代码仍然是开放的。断言和进入可执行文件的分析代码... @Keith 是正确的,在内部使用它。 - Chris Huang-Leaver
据我所知,动态链接尚未在法庭上进行过测试。虽然自由软件基金会认为这是不允许的,但其他人有不同的看法。维基百科对此有一个很好的讨论。http://en.wikipedia.org/wiki/GNU_General_Public_License#Linking_and_derived_works - KeithB

6
这里有一种替代方法。一个debug_assert()宏会以编程方式设置一个条件断点。如果你在调试器中运行,当assert表达式为false时,你将会遇到断点,并且你可以分析实时堆栈(程序不会终止)。如果你没有在调试器中运行,一个失败的debug_assert()会导致程序中止,并且你可以从其中获取一个核心转储来分析堆栈(参见我之前的答案)。
与普通assert相比,这种方法的优势在于,在触发debug_assert后(在调试器中运行时),你可以继续运行程序。换句话说,debug_assert()比assert()略具灵活性。
   #include <iostream>
   #include <cassert>
   #include <sys/resource.h> 

// note: The assert expression should show up in
// stack trace as parameter to this function
void debug_breakpoint( char const * expression )
   {
   asm("int3"); // x86 specific
   }

#ifdef NDEBUG
   #define debug_assert( expression )
#else
// creates a conditional breakpoint
   #define debug_assert( expression ) \
      do { if ( !(expression) ) debug_breakpoint( #expression ); } while (0)
#endif

void recursive( int i=0 )
   {
   debug_assert( i < 5 );
   if ( i < 10 ) recursive(i+1);
   }

int main( int argc, char * argv[] )
   {
   rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };
   setrlimit( RLIMIT_CORE, &core_limit ); // enable core dumps
   recursive();
   }

注意:有时在调试器中设置的“条件断点”可能会很慢。通过以编程方式建立断点,此方法的性能应与普通assert()相当。

注意:如所述,这是针对英特尔x86架构的--其他处理器可能具有不同的指令来生成断点。


1
我曾经使用过类似的东西,但是使用了一个空函数debug_breakpoint。在调试时,我只需在gdb提示符下输入“bre debug_breakpoint”即可 - 不需要汇编语言(将debug_breakpoint编译到单独的编译单元中以避免被优化掉)。 - Axel

5

OP正在要求堆栈跟踪,而不是日志记录。 - chrisaycock
5
Glog具有打印堆栈跟踪的功能。 - Industrial-antidepressant
4
确实,glog有堆栈跟踪(http://google-glog.googlecode.com/svn/trunk/doc/glog.html故障信号处理程序部分),但它没有代码行信息。 - dimba
我没有时间尝试addr2line,但它可能是一个解决方案。 - Industrial-antidepressant
5
google-glog是backtrace和backtrace_symbols的简单封装。它不会给你文件名和行号。 - Sarang

5
有点晚了,但是您可以使用 libbfb 来获取文件名和行号,就像symsnarf.c中的refdbg一样。 libbfb被addr2linegdb内部使用。

5

这是我的解决方案:

#include <execinfo.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <zconf.h>
#include "regex"

std::string getexepath() {
    char result[PATH_MAX];
    ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
    return std::string(result, (count > 0) ? count : 0);
}

std::string sh(std::string cmd) {
    std::array<char, 128> buffer;
    std::string result;
    std::shared_ptr<FILE> pipe(popen(cmd.c_str(), "r"), pclose);
    if (!pipe) throw std::runtime_error("popen() failed!");
    while (!feof(pipe.get())) {
        if (fgets(buffer.data(), 128, pipe.get()) != nullptr) {
            result += buffer.data();
        }
    }
    return result;
}


void print_backtrace(void) {
    void *bt[1024];
    int bt_size;
    char **bt_syms;
    int i;

    bt_size = backtrace(bt, 1024);
    bt_syms = backtrace_symbols(bt, bt_size);
    std::regex re("\\[(.+)\\]");
    auto exec_path = getexepath();
    for (i = 1; i < bt_size; i++) {
        std::string sym = bt_syms[i];
        std::smatch ms;
        if (std::regex_search(sym, ms, re)) {
            std::string addr = ms[1];
            std::string cmd = "addr2line -e " + exec_path + " -f -C " + addr;
            auto r = sh(cmd);
            std::regex re2("\\n$");
            auto r2 = std::regex_replace(r, re2, "");
            std::cout << r2 << std::endl;
        }
    }
    free(bt_syms);
}

void test_m() {
    print_backtrace();
}

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

输出:

/home/roroco/Dropbox/c/ro-c/cmake-build-debug/ex/test_backtrace_with_line_number
test_m()
/home/roroco/Dropbox/c/ro-c/ex/test_backtrace_with_line_number.cpp:57
main
/home/roroco/Dropbox/c/ro-c/ex/test_backtrace_with_line_number.cpp:61
??
??:0

"

??

和"

??:0

",因为这个跟踪信息在libc中而不是我的源代码中。"

2
一种解决方案是在失败的assert处理程序中启动一个带有“bt”脚本的gdb。虽然集成这样的gdb启动不是很容易,但它将为您提供回溯、参数和反编译名称(或者您可以通过c++filt程序传递gdb输出)。
这两个程序(gdb和c++filt)都不会链接到您的应用程序中,因此GPL不会要求您开源完整的应用程序。
您可以使用相同的方法(执行GPL程序)来处理回溯符号。只需生成%eip的ascii列表和exec文件的映射(/proc/self/maps),并将其传递给单独的二进制文件即可。

2
您可以使用DeathHandler - 这是一个小型的C++类,它可以为您完成所有操作,非常可靠。

иҝҷдёҺжүӢеҠЁжү§иЎҢaddr2lineзӣёеҗҢгҖӮ - rustyx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接