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

How to get a stack trace for C++ using gcc with line number information?

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

我发现GCC的backtrace()backtrace_symbols()方法不够:

  • 名字乱七八糟
  • 无行信息
  • 第一个问题可以通过abi::uucxa_demangle解决。

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

    悬停器代码是GNU许可证,所以imho我不能在商业代码中使用它。

    有什么建议吗?

    附笔。

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

    PS 2

    类似问题(谢谢Nobar)


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

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

    用这个问题发布的例子对我来说并没有写的那么好,所以这里是我的"固定"版本(我在Ubuntu9.04上运行这个)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/wait.h>
    #include <unistd.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;
        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);
        }
    }

    如参考问题所示,gdb提供了您可以使用的其他选项。例如,使用"bt full"而不是"bt"生成更详细的报告(输出中包含局部变量)。gdb的手册页很轻,但这里提供了完整的文档。

    因为这是基于gdb的,所以输出包括散乱的名称、行号、函数参数,甚至可选的局部变量。另外,gdb是线程感知的,所以您应该能够提取一些特定于线程的元数据。

    下面是我用这个方法看到的堆栈跟踪类型的一个例子。

    1
    2
    3
    4
    5
    6
    7
    8
    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"的第二个实例应用于进程)。


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

    • 方法4:

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

    基本上,它使用addr2行将地址转换为文件名和行号。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    #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

    程序输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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


    在我的GCC C++应用程序崩溃时,如何生成一个StActTrace:提供了许多建议,包括关于如何在运行时生成堆栈跟踪的大量讨论。

    该线程中我个人最喜欢的答案是启用核心转储,它允许您查看崩溃时的完整应用程序状态(包括函数参数、行号和未组合的名称)。这种方法的另一个好处是,它不仅适用于断言,还适用于分段错误和未处理的异常。

    不同的Linux shell使用不同的命令来启用核心转储,但是您可以在应用程序代码中用类似的方法来实现它…

    1
    2
    3
    4
    #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

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

    1
    $ kdbg executable core

    以下是一些示例输出…

    alt text

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ ( 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


    由于GPL许可代码旨在在开发过程中帮助您,因此您不能将其包含在最终产品中。GPL限制您分发与非GPL兼容代码链接的GPL许可证代码。只要你只在内部使用GPL代码,你就可以了。


    使用GoogleGlog库。它有新的BSD许可证。

    它在stacktrace.h文件中包含getstacktrace函数。

    编辑

    我在这里找到了http://blog.bigpixel.ro/2010/09/09/stack-unwinding-stack-trace-with-gcc/一个名为addr2line的实用程序,它将程序地址转换为文件名和行号。

    http://linuxcommand.org/man_pages/addr2line1.html


    这是另一种方法。调试断言()宏以编程方式设置条件断点。如果您在调试器中运行,那么当断言表达式为假时,您将遇到一个断点——并且您可以分析活动堆栈(程序不会终止)。如果您没有在调试程序中运行,失败的调试断言()会导致程序中止,并且您会得到一个核心转储,从中可以分析堆栈(请参阅我前面的回答)。

    与普通断言相比,这种方法的优点是,您可以在触发调试断言后(在调试器中运行时)继续运行程序。换言之,debug ause()比assert()稍微灵活一些。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
       #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()。

    注:如文所述,这是特定于Intel x86体系结构的——其他处理器可能有不同的指令来生成断点。


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


    你可以使用DeaHealther-TrimeC++类,它为你做任何事情,可靠。


    解决方案之一是在失败的断言处理程序中使用"bt"脚本启动gdb。集成这样的GDB启动不是很容易,但它会给您回溯和ARG和DEMANK名称(或者您可以通过C++ FILT程序传递GDB输出)。

    这两个程序(GDB和C++ + FLT)都不会链接到你的应用程序,所以GPL不需要你去完成完整的应用程序。

    您可以使用与回溯符号相同的方法(执行GPL程序)。只需生成%eip的ascii列表和exec文件的map(/proc/self/map s),并将其传递到单独的二进制文件。


    这是我的第三个答案——仍然试图利用核心转储。

    在这个问题上还不完全清楚"类断言"宏是应该终止应用程序(就像断言那样),还是应该在生成堆栈跟踪后继续执行。

    在这个答案中,我将处理您希望在其中显示堆栈跟踪并继续执行的情况。我在下面编写了core dump()函数来生成一个核心转储,自动从中提取堆栈跟踪,然后继续执行程序。

    用法与assert()相同。当然,不同之处在于assert()终止程序,但coredump assert()不终止程序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
       #include <iostream>
       #include <sys/resource.h>
       #include <cstdio>
       #include <cstdlib>
       #include <boost/lexical_cast.hpp>
       #include <string>
       #include <sys/wait.h>
       #include <unistd.h>

       std::string exename;

    // expression argument is for diagnostic purposes (shows up in call-stack)
    void coredump( char const * expression )
       {

       pid_t childpid = fork();

       if ( childpid == 0 ) // child process generates core dump
          {
          rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };
          setrlimit( RLIMIT_CORE, &core_limit ); // enable core dumps
          abort(); // terminate child process and generate core dump
          }

    // give each core-file a unique name
       if ( childpid > 0 ) waitpid( childpid, 0, 0 );
       static int count=0;
       using std::string;
       string pid = boost::lexical_cast<string>(getpid());
       string newcorename ="core-"+boost::lexical_cast<string>(count++)+"."+pid;
       string rawcorename ="core."+boost::lexical_cast<string>(childpid);
       int rename_rval = rename(rawcorename.c_str(),newcorename.c_str()); // try with core.PID
       if ( rename_rval == -1 ) rename_rval = rename("core",newcorename.c_str()); // try with just core
       if ( rename_rval == -1 ) std::cerr<<"failed to capture core file
    "
    ;

      #if 1 // optional: dump stack trace and delete core file
       string cmd ="( CMDFILE=$(mktemp); echo 'bt' >${CMDFILE}; gdb 2>/dev/null --batch -x ${CMDFILE}"+exename+""+newcorename+" ; unlink ${CMDFILE} )";
       int system_rval = system( ("bash -c '"+cmd+"'").c_str() );
       if ( system_rval == -1 ) std::cerr.flush(), perror("system() failed during stack trace"), fflush(stderr);
       unlink( newcorename.c_str() );
      #endif

       }

    #ifdef NDEBUG
       #define coredump_assert( expression ) ((void)(expression))
    #else
       #define coredump_assert( expression ) do { if ( !(expression) ) { coredump( #expression ); } } while (0)
    #endif

    void recursive( int i=0 )
       {
       coredump_assert( i < 2 );
       if ( i < 4 ) recursive(i+1);
       }

    int main( int argc, char * argv[] )
       {
       exename = argv[0]; // this is used to generate the stack trace
       recursive();
       }

    当我运行程序时,它显示三个堆栈跟踪…

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    Core was generated by `./temp.exe'.                                        
    Program terminated with signal 6, Aborted.
    [New process 24251]
    #0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
    #0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
    #1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
    #2  0x0000000000401a0e in coredump (expression=0x403303"i < 2") at ./demo3.cpp:29
    #3  0x0000000000401f5f in recursive (i=2) at ./demo3.cpp:60
    #4  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
    #5  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
    #6  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66
    Core was generated by `./temp.exe'
    .
    Program terminated with signal 6, Aborted.
    [New process 24259]
    #0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
    #0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
    #1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
    #2  0x0000000000401a0e in coredump (expression=0x403303"i < 2") at ./demo3.cpp:29
    #3  0x0000000000401f5f in recursive (i=3) at ./demo3.cpp:60
    #4  0x0000000000401f70 in recursive (i=2) at ./demo3.cpp:61
    #5  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
    #6  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
    #7  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66
    Core was generated by `./temp.exe'.
    Program terminated with signal 6, Aborted.
    [New process 24267]
    #0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
    #0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
    #1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
    #2  0x0000000000401a0e in coredump (expression=0x403303"i < 2") at ./demo3.cpp:29
    #3  0x0000000000401f5f in recursive (i=4) at ./demo3.cpp:60
    #4  0x0000000000401f70 in recursive (i=3) at ./demo3.cpp:61
    #5  0x0000000000401f70 in recursive (i=2) at ./demo3.cpp:61
    #6  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
    #7  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
    #8  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66


    我想行号和当前的EIP值有关,对吧?

    解决方案1:然后,您可以使用getthreadcontext()之类的东西,除了您在Linux上工作之外。我搜索了一下,发现了类似的东西,ptrace():

    The ptrace() system call provides a
    means by which a parent process may
    observe and control the execution of
    another process, and examine and
    change its core image and registers. [...]
    The parent can initiate a trace by
    calling fork(2) and having the
    resulting child do a PTRACE_TRACEME,
    followed (typically) by an exec(3).
    Alternatively, the parent may commence
    trace of an existing process using
    PTRACE_ATTACH.

    现在我在想,你可以做一个"主"程序,检查发送给孩子的信号,这是你正在工作的真正程序。在fork()之后,它调用waitid():

    All of these system calls are used to
    wait for state changes in a child of
    the calling process, and obtain
    information about the child whose
    state has changed.

    如果捕捉到一个sigsegv(或类似的东西),调用ptrace()获得eip的值。

    PS:我从来没有使用过这些系统调用(实际上,我以前从未见过它们;)所以我不知道这是否可能,也不能帮助您。至少我希望这些链接有用。;)

    解决方案2:第一个解决方案相当复杂,对吗?我想出了一个更简单的方法:使用signal()捕获您感兴趣的信号,并调用一个简单的函数来读取存储在堆栈中的eip值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ...
    signal(SIGSEGV, sig_handler);
    ...

    void sig_handler(int signum)
    {
        int eip_value;

        asm {
            push eax;
            mov eax, [ebp - 4]
            mov eip_value, eax
            pop eax
        }

        // now you have the address of the
        // **next** instruction after the
        // SIGSEGV was received
    }

    asm语法是Borland的语法,只需将其适应GAS。;)


    这是我的解决方案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    #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("\
    $"
    );
                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;
    }

    输出:

    1
    2
    3
    4
    5
    6
    7
    /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中,而不是在我的源中