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

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

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


当前回答

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

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

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

其他回答

城里的新国王来了 https://github.com/bombela/backward-cpp

在代码中放置1个头文件,安装1个库。

我个人使用这个函数来调用它

#include "backward.hpp"
void stacker() {

using namespace backward;
StackTrace st;


st.load_here(99); //Limit the number of trace depth to 99
st.skip_n_firsts(3);//This will skip some backward internal function from the trace

Printer p;
p.snippet = true;
p.object = true;
p.color = true;
p.address = true;
p.print(st, stderr);
}

需要注意的是,一旦生成了核心文件,就需要使用gdb工具来查看它。为了让gdb理解你的核心文件,你必须告诉gcc用调试符号来检测二进制文件:要做到这一点,你需要使用-g标志进行编译:

$ g++ -g prog.cpp -o prog

然后,您可以设置“ulimit -c unlimited”来转储一个核心,或者只是在gdb中运行您的程序。我更喜欢第二种方法:

$ gdb ./prog
... gdb startup output ...
(gdb) run
... program runs and crashes ...
(gdb) where
... gdb outputs your stack trace ...

我希望这能有所帮助。

我迄今为止最好的异步信号安全尝试

如果不安全请告诉我。我还没有找到显示行号的方法。

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

#define TRACE_MAX 1024

void handler(int sig) {
    (void)sig;
    void *array[TRACE_MAX];
    size_t size;
    const char msg[] = "failed with a signal\n";

    size = backtrace(array, TRACE_MAX);
    write(STDERR_FILENO, msg, sizeof(msg));
    backtrace_symbols_fd(array, size, STDERR_FILENO);
    _Exit(1);
}

void my_func_2(void) {
    *((int*)0) = 1;
}

void my_func_1(double f) {
    (void)f;
    my_func_2();
}

void my_func_1(int i) {
    (void)i;
    my_func_2();
}

int main() {
    /* Make a dummy call to `backtrace` to load libgcc because man backrace says:
     *    *  backtrace() and backtrace_symbols_fd() don't call malloc() explicitly, but they are part of libgcc, which gets loaded dynamically when first used.  Dynamic loading usually triggers a call to mal‐
     *       loc(3).  If you need certain calls to these two functions to not allocate memory (in signal handlers, for example), you need to make sure libgcc is loaded beforehand.
     */
    void *dummy[1];
    backtrace(dummy, 1);
    signal(SIGSEGV, handler);

    my_func_1(1);
}

编译并运行:

g++ -ggdb3 -O2 -std=c++11 -Wall -Wextra -pedantic -rdynamic -o stacktrace_on_signal_safe.out stacktrace_on_signal_safe.cpp
./stacktrace_on_signal_safe.out

需要使用-rdynamic来获取函数名:

failed with a signal
./stacktrace_on_signal_safe.out(_Z7handleri+0x6e)[0x56239398928e]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f04b1459520]
./stacktrace_on_signal_safe.out(main+0x38)[0x562393989118]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f04b1440d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f04b1440e40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x562393989155]

然后,我们可以将它管道到c++filt中以demangle:

./stacktrace_on_signal_safe.out |& c++filt

给:

failed with a signal
/stacktrace_on_signal_safe.out(handler(int)+0x6e)[0x55b6df43f28e]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f40d4167520]
./stacktrace_on_signal_safe.out(main+0x38)[0x55b6df43f118]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f40d414ed90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f40d414ee40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x55b6df43f155]

由于优化,几个级别都丢失了,使用-O0我们得到一个更完整的:

/stacktrace_on_signal_safe.out(handler(int)+0x76)[0x55d39b68325f]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f4d8ffdd520]
./stacktrace_on_signal_safe.out(my_func_2()+0xd)[0x55d39b6832bb]
./stacktrace_on_signal_safe.out(my_func_1(int)+0x14)[0x55d39b6832f1]
./stacktrace_on_signal_safe.out(main+0x4a)[0x55d39b68333e]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f4d8ffc4d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f4d8ffc4e40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x55d39b683125]

行号不存在,但我们可以通过addr2line获取它们。这需要不使用-rdynamic进行构建:

g++ -ggdb3 -O0 -std=c++23 -Wall -Wextra -pedantic -o stacktrace_on_signal_safe.out stacktrace_on_signal_safe.cpp
./stacktrace_on_signal_safe.out |& sed -r 's/.*\(//;s/\).*//' | addr2line -C -e stacktrace_on_signal_safe.out -f

生产:

??
??:0
handler(int)
/home/ciro/stacktrace_on_signal_safe.cpp:14
??
??:0
my_func_2()
/home/ciro/stacktrace_on_signal_safe.cpp:22
my_func_1(i
/home/ciro/stacktrace_on_signal_safe.cpp:33
main
/home/ciro/stacktrace_on_signal_safe.cpp:45
??
??:0
??
??:0
_start
??:?

Awk解析出非-rdynamic输出的+<addr>编号:

./stacktrace_on_signal_safe.out(+0x125f)[0x55984828825f]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f8644a1e520]
./stacktrace_on_signal_safe.out(+0x12bb)[0x5598482882bb]
./stacktrace_on_signal_safe.out(+0x12f1)[0x5598482882f1]
./stacktrace_on_signal_safe.out(+0x133e)[0x55984828833e]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f8644a05d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f8644a05e40]
./stacktrace_on_signal_safe.out(+0x1125)[0x559848288125]

如果你还想将实际的信号数打印到stdout,这里有一个异步信号安全实现int到字符串:使用write或异步安全函数从信号处理程序打印int,因为printf不是。

在Ubuntu 22.04上测试。

C++23 <stacktrace>

与许多其他答案一样,本节忽略了问题的异步信号安全方面,这可能导致代码在崩溃时死锁,这可能会很严重。我们只希望有一天c++标准会添加一个boost::stacktrace::safe_dump_to类函数来一劳永逸地解决这个问题。

这将是一般更优秀的c++堆栈跟踪选项,正如前面提到的:在C或c++中打印调用堆栈,因为它显示行号并自动为我们执行需求。

stacktrace_on_signal.cpp

#include <stacktrace>
#include <iostream>

#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

void handler(int sig) {
    (void)sig;
    /* De-register this signal in the hope of avoiding infinite loops
     * if asyns signal unsafe things fail later on. But can likely still deadlock. */
    signal(sig, SIG_DFL);
    // std::stacktrace::current
    std::cout << std::stacktrace::current();
    // C99 async signal safe version of exit().
    _Exit(1);
}

void my_func_2(void) {
    *((int*)0) = 1;
}

void my_func_1(double f) {
    (void)f;
    my_func_2();
}

void my_func_1(int i) {
    (void)i;
    my_func_2();
}

int main() {
    signal(SIGSEGV, handler);
    my_func_1(1);
}

编译并运行:

g++ -ggdb3 -O2 -std=c++23 -Wall -Wextra -pedantic -o stacktrace_on_signal.out stacktrace_on_signal.cpp -lstdc++_libbacktrace
./stacktrace_on_signal.out

在GCC 12.1上从源代码编译的输出,Ubuntu 22.04:

   0# handler(int) at /home/ciro/stacktrace_on_signal.cpp:11
   1#      at :0
   2# my_func_2() at /home/ciro/stacktrace_on_signal.cpp:16
   3#      at :0
   4#      at :0
   5#      at :0
   6#

我认为它错过了my_func_1,由于优化被打开,通常没有什么我们可以做的AFAIK。用-O0代替会更好:

   0# handler(int) at /home/ciro/stacktrace_on_signal.cpp:11
   1#      at :0
   2# my_func_2() at /home/ciro/stacktrace_on_signal.cpp:16
   3# my_func_1(int) at /home/ciro/stacktrace_on_signal.cpp:26
   4#      at /home/ciro/stacktrace_on_signal.cpp:31
   5#      at :0
   6#      at :0
   7#      at :0
   8#

但不知道为什么梅恩没有出现。

backtrace_simple

https://github.com/gcc-mirror/gcc/blob/releases/gcc-12.1.0/libstdc%2B%2B-v3/src/libbacktrace/backtrace-supported.h.in#L45提到backtrace_simple是安全的:

/* BACKTRACE_USES_MALLOC will be #define'd as 1 if the backtrace
   library will call malloc as it works, 0 if it will call mmap
   instead.  This may be used to determine whether it is safe to call
   the backtrace functions from a signal handler.  In general this
   only applies to calls like backtrace and backtrace_pcinfo.  It does
   not apply to backtrace_simple, which never calls malloc.  It does
   not apply to backtrace_print, which always calls fprintf and
   therefore malloc.  */

但它使用起来似乎不太方便,主要是一个内部工具。

标准::basic_stacktrace

这就是std::stacktrace的基础,根据:https://en.cppreference.com/w/cpp/utility/basic_stacktrace

它有一个分配器参数,cppreference将其描述为:

为在热路径或嵌入式环境中使用basic_stacktrace提供了自定义分配器的支持。用户可以在堆栈上或其他适当的地方分配stacktrace_entry对象。

所以我想知道如果basic_stacktrace本身是异步信号安全的,如果它不可能使std::stacktrace的一个版本也与自定义分配器,例如:

写入磁盘上的文件,如boost::stacktrace::safe_dump_to 或者写入某个预先分配的具有最大大小的堆栈缓冲区

https://apolukhin.github.io/papers/stacktrace_r1.html可能是提案,提到:

关于信号安全的注意:本建议并不试图为捕获和解码堆栈跟踪提供信号安全的解决方案。这种功能目前还不能在一些流行的平台上实现。然而,本文试图提供一个可扩展的解决方案,通过提供一个信号安全分配器和改变堆栈跟踪实现细节,有可能使信号安全。

只是得到核心转储吗?

核心转储允许您使用GDB检查内存:当程序的核心转储文件具有命令行参数时,如何使用GDB分析它?所以它比只有痕迹更强大。

只要确保你正确地启用了它,特别是在Ubuntu 22.04上,你需要:

echo 'core' | sudo tee /proc/sys/kernel/core_pattern

或者要学习使用apport,请参见:https://askubuntu.com/questions/1349047/where-do-i-find-core-dump-files-and-how-do-i-view-and-analyze-the-backtrace-st/1442665#1442665

尽管已经提供了描述如何使用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);
}

作为一种仅适用于Windows的解决方案,您可以使用Windows错误报告(Error Reporting)获得相当于堆栈跟踪的信息(包含多得多的信息)。只需几个注册表项,就可以设置为收集用户模式转储:

从Windows Server 2008和Windows Vista with Service Pack 1 (SP1)开始,可以配置Windows错误报告(WER),以便在用户模式应用程序崩溃后收集完整的用户模式转储并存储在本地。[…] 默认情况下不启用此特性。启用该特性需要管理员权限。要启用和配置该功能,请在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps键下使用以下注册表值。

您可以从具有所需权限的安装程序中设置注册表项。

创建用户模式转储比在客户端生成堆栈跟踪有以下优点:

It's already implemented in the system. You can either use WER as outlined above, or call MiniDumpWriteDump yourself, if you need more fine-grained control over the amount of information to dump. (Make sure to call it from a different process.) Way more complete than a stack trace. Among others it can contain local variables, function arguments, stacks for other threads, loaded modules, and so on. The amount of data (and consequently size) is highly customizable. No need to ship debug symbols. This both drastically decreases the size of your deployment, as well as makes it harder to reverse-engineer your application. Largely independent of the compiler you use. Using WER does not even require any code. Either way, having a way to get a symbol database (PDB) is very useful for offline analysis. I believe GCC can either generate PDB's, or there are tools to convert the symbol database to the PDB format.

请注意,WER只能由应用程序崩溃触发(即系统由于未处理的异常而终止进程)。MiniDumpWriteDump可以在任何时候调用。如果您需要转储当前状态以诊断崩溃以外的问题,这可能会很有帮助。

如果你想评估mini dump的适用性,必读:

有效minidumps 有效的小排量(第二部分)