我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。

我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。

我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?


当前回答

一些版本的libc包含处理堆栈跟踪的函数;你可能会用到它们:

http://www.gnu.org/software/libc/manual/html_node/Backtraces.html

我记得很久以前使用过libunwind来获取堆栈跟踪,但您的平台可能不支持它。

其他回答

尽管已经提供了描述如何使用GNU libc backtrace()函数1的正确答案,并且我提供了自己的答案,描述了如何确保从信号处理程序的回溯指向fault2的实际位置,但我没有看到任何从回溯输出的要求c++符号的提及。

当从c++程序获得回溯时,可以通过c++filt1运行输出来要求符号,或者直接使用abi::__cxa_demangle1。

1 Linux & OS X 注意,c++filt和__cxa_demangle是特定于GCC的 2 Linux


下面的c++ Linux示例使用与我的其他答案相同的信号处理程序,并演示如何使用c++filt来要求符号。

代码:

class foo
{
public:
    foo() { foo1(); }

private:
    void foo1() { foo2(); }
    void foo2() { foo3(); }
    void foo3() { foo4(); }
    void foo4() { crash(); }
    void crash() { char * p = NULL; *p = 0; }
};

int main(int argc, char ** argv)
{
    // Setup signal handler for SIGSEGV
    ...

    foo * f = new foo();
    return 0;
}

输出(. /测试):

signal 11 (Segmentation fault), address is (nil) from 0x8048e07
[bt]: (1) ./test(crash__3foo+0x13) [0x8048e07]
[bt]: (2) ./test(foo4__3foo+0x12) [0x8048dee]
[bt]: (3) ./test(foo3__3foo+0x12) [0x8048dd6]
[bt]: (4) ./test(foo2__3foo+0x12) [0x8048dbe]
[bt]: (5) ./test(foo1__3foo+0x12) [0x8048da6]
[bt]: (6) ./test(__3foo+0x12) [0x8048d8e]
[bt]: (7) ./test(main+0xe0) [0x8048d18]
[bt]: (8) ./test(__libc_start_main+0x95) [0x42017589]
[bt]: (9) ./test(__register_frame_info+0x3d) [0x8048981]

需求输出(。/test 2>&1 | c++filt):

signal 11 (Segmentation fault), address is (nil) from 0x8048e07
[bt]: (1) ./test(foo::crash(void)+0x13) [0x8048e07]
[bt]: (2) ./test(foo::foo4(void)+0x12) [0x8048dee]
[bt]: (3) ./test(foo::foo3(void)+0x12) [0x8048dd6]
[bt]: (4) ./test(foo::foo2(void)+0x12) [0x8048dbe]
[bt]: (5) ./test(foo::foo1(void)+0x12) [0x8048da6]
[bt]: (6) ./test(foo::foo(void)+0x12) [0x8048d8e]
[bt]: (7) ./test(main+0xe0) [0x8048d18]
[bt]: (8) ./test(__libc_start_main+0x95) [0x42017589]
[bt]: (9) ./test(__register_frame_info+0x3d) [0x8048981]

下面的代码构建在我原来答案中的信号处理程序之上,可以替换上面示例中的信号处理程序,以演示如何使用abi::__cxa_demangle来提取符号。此信号处理程序产生与上面示例相同的需求输出。

代码:

void crit_err_hdlr(int sig_num, siginfo_t * info, void * ucontext)
{
    sig_ucontext_t * uc = (sig_ucontext_t *)ucontext;

    void * caller_address = (void *) uc->uc_mcontext.eip; // x86 specific

    std::cerr << "signal " << sig_num 
              << " (" << strsignal(sig_num) << "), address is " 
              << info->si_addr << " from " << caller_address 
              << std::endl << std::endl;

    void * array[50];
    int size = backtrace(array, 50);

    array[1] = caller_address;

    char ** messages = backtrace_symbols(array, size);    

    // skip first stack frame (points here)
    for (int i = 1; i < size && messages != NULL; ++i)
    {
        char *mangled_name = 0, *offset_begin = 0, *offset_end = 0;

        // find parantheses and +address offset surrounding mangled name
        for (char *p = messages[i]; *p; ++p)
        {
            if (*p == '(') 
            {
                mangled_name = p; 
            }
            else if (*p == '+') 
            {
                offset_begin = p;
            }
            else if (*p == ')')
            {
                offset_end = p;
                break;
            }
        }

        // if the line could be processed, attempt to demangle the symbol
        if (mangled_name && offset_begin && offset_end && 
            mangled_name < offset_begin)
        {
            *mangled_name++ = '\0';
            *offset_begin++ = '\0';
            *offset_end++ = '\0';

            int status;
            char * real_name = abi::__cxa_demangle(mangled_name, 0, 0, &status);

            // if demangling is successful, output the demangled function name
            if (status == 0)
            {    
                std::cerr << "[bt]: (" << i << ") " << messages[i] << " : " 
                          << real_name << "+" << offset_begin << offset_end 
                          << std::endl;

            }
            // otherwise, output the mangled function name
            else
            {
                std::cerr << "[bt]: (" << i << ") " << messages[i] << " : " 
                          << mangled_name << "+" << offset_begin << offset_end 
                          << std::endl;
            }
            free(real_name);
        }
        // otherwise, print the whole line
        else
        {
            std::cerr << "[bt]: (" << i << ") " << messages[i] << std::endl;
        }
    }
    std::cerr << std::endl;

    free(messages);

    exit(EXIT_FAILURE);
}

在Linux/unix/MacOSX上使用核心文件(您可以通过ulimit或兼容的系统调用启用它们)。在Windows上使用Microsoft错误报告(您可以成为合作伙伴并访问您的应用程序崩溃数据)。

* nix: 你可以拦截SIGSEGV(通常这个信号是在崩溃之前发出的),并将信息保存到一个文件中。(除了可以使用GDB进行调试的核心文件之外)。

赢得: 从msdn检查这个。

你也可以看看谷歌的chrome代码,看看它是如何处理崩溃的。它有一个很好的异常处理机制。

忘记改变你的源代码,用backtrace()函数或宏来做一些hack -这些只是糟糕的解决方案。

作为一个有效的解决方案,我的建议是:

用"-g"标志编译程序,用于将调试符号嵌入二进制(不用担心,这不会影响您的性能)。 在linux上运行下一个命令:"ulimit -c unlimited" -允许系统进行大崩溃转储。 当你的程序崩溃时,在工作目录中你会看到文件“core”。 gdb -batch -ex "backtrace" ./your_program_exe ./core . exe .执行下一个命令,将backtrace输出到标准输出

这将以人类可读的方式(包含源文件名和行号)打印程序的正确可读回溯。 此外,这种方法将给你自由自动化你的系统: 编写一个简短的脚本,检查进程是否创建了核心转储,然后通过电子邮件向开发人员发送回溯信息,或将其记录到一些日志系统中。

You are probably not going to like this - all I can say in its favour is that it works for me, and I have similar but not identical requirements: I am writing a compiler/transpiler for a 1970's Algol-like language which uses C as it's output and then compiles the C so that as far as the user is concerned, they're generally not aware of C being involved, so although you might call it a transpiler, it's effectively a compiler that uses C as it's intermediate code. The language being compiled has a history of providing good diagnostics and a full backtrace in the original native compilers. I've been able to find gcc compiler flags and libraries etc that allow me to trap most of the runtime errors that the original compilers did (although with one glaring exception - unassigned variable trapping). When a runtime error occurs (eg arithmetic overflow, divide by zero, array index out of bounds, etc) the original compilers output a backtrace to the console listing all variables in the stack frames of every active procedure call. I struggled to get this effect in C, but eventually did so with what can only be described as a hack... When the program is invoked, the wrapper that supplies the C "main" looks at its argv, and if a special option is not present, it restarts itself under gdb with an altered argv containing both gdb options and the 'magic' option string for the program itself. This restarted version then hides those strings from the user's code by restoring the original arguments before calling the main block of the code written in our language. When an error occurs (as long as it is not one explicitly trapped within the program by user code), it exits to gdb which prints the required backtrace.

启动序列中的关键代码行包括:

  if ((argc >= 1) && (strcmp(origargv[argc-1], "--restarting-under-gdb")) != 0) {
    // initial invocation
    // the "--restarting-under-gdb" option is how the copy running under gdb knows
    // not to start another gdb process.

and

  char *gdb [] = {
    "/usr/bin/gdb", "-q", "-batch", "-nx", "-nh", "-return-child-result",
    "-ex", "run",
    "-ex", "bt full",
    "--args"
  };

The original arguments are appended to the gdb options above. That should be enough of a hint for you to do something similar for your own system. I did look at other library-supported backtrace options (eg libbacktrace, https://codingrelic.geekhold.com/2010/09/gcc-function-instrumentation.html, etc) but they only output the procedure call stack, not the local variables. However if anyone knows of any cleaner mechanism to get a similar effect, do please let us know. The main downside to this is that the variables are printed in C syntax, not the syntax of the language the user writes in. And (until I add suitable #line directives on every generated line of C :-() the backtrace lists the C source file and line numbers.

G PS我使用的gcc编译选项是:

 GCCOPTS=" -Wall -Wno-return-type -Wno-comment -g -fsanitize=undefined
 -fsanitize-undefined-trap-on-error -fno-sanitize-recover=all -frecord-gcc-switches
 -fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow -ftrapv
 -grecord-gcc-switches -O0 -ggdb3 "