当抛出异常时如何显示堆栈跟踪

266
我想要有一种方法,如果出现异常,可以向用户报告堆栈跟踪。最好的方法是什么?
如果可能的话,我希望它是可移植的。我希望信息能够弹出,这样用户就可以复制堆栈跟踪并通过电子邮件发送给我,以便在出现错误时。
16个回答

93
Andrew Grant的回答并不能帮助获取抛出函数的堆栈跟踪,至少在使用GCC时是这样的,因为throw语句本身不会保存当前的堆栈跟踪,而catch处理程序在那一点上将无法访问堆栈跟踪。
唯一的方法 - 使用GCC - 解决这个问题是确保在throw指令的位置生成堆栈跟踪,并将其保存在异常对象中。
当然,这种方法要求每个抛出异常的代码都使用特定的异常类。 更新于2017年7月11日:对于一些有用的代码,请查看cahit beyaz的回答,它指向http://stacktrace.sourceforge.net - 我还没有使用过,但它看起来很有前途。 更新于2023年7月29日:截至2023年7月的堆栈跟踪库有:
  • C++23 <stacktrace>:C++23将引入<stacktrace>,一些标准库实现已经支持或部分支持。
  • boost stacktrace:由作者提出的<stacktrace>参考实现。它功能齐全,但需要各种配置和依赖。
  • backward-cpp:一个广泛使用的库,提供了大量信息,包括每个帧的代码片段。根据您的系统,它有各种配置和依赖。它支持除mingw之外的大多数平台。
  • cpptrace:一个较新的C++堆栈跟踪库,简单、便携且自包含。

2
很遗憾,该链接已失效。您能提供其他链接吗? - warran
2
而且archive.org也不知道。该死。好吧,程序应该很清楚:抛出一个自定义类的对象,在抛出时记录堆栈跟踪。 - Thomas Tempelmann
1
在 StackTrace 的主页上,我看到了 throw stack_runtime_error。我推断这个库只适用于从该类派生的异常,而不适用于 std::exception 或第三方库的异常,我的理解正确吗? - Thomas
14
很遗憾,答案是否定的,“你无法从C++异常获取堆栈跟踪”,唯一的选择是抛出自己的类,在构造时生成堆栈跟踪。如果你必须使用C++标准库中的任何部分,那么你将没有办法。很抱歉,你只能接受这个事实。 - Code Abominator
@CodeAbominator 这还是不是我们无法获取 std:: 异常的堆栈跟踪的情况吗?我尝试了 cpptrace。它要求我们使用它自己的异常类,并且无法捕获 std:: 异常。Boost stacktrace、backward-cpp 或其他库能够捕获 std:: 异常吗? - undefined

81

这取决于所使用的平台。

在GCC上很简单,详见此帖子获取更多细节。

在MSVC上,您可以使用StackWalker库处理Windows所需的所有底层API调用。

您需要找到将此功能集成到应用程序中的最佳方法,但是您需要编写的代码量应该很小。


98
您提供的链接大多是指从段错误生成跟踪,但提问者明确提到了异常,这是一种完全不同的问题。 - Shep
13
我同意@Shep的观点-这个答案并没有真正帮助在GCC上获取抛出代码的堆栈跟踪。请查看我的答案,了解可能的解决方案。 - Thomas Tempelmann
6
答案有误导性。该链接指向特定于“Linux”而非“gcc”的答案。 - fjardon
你可以按照这个答案的说明,覆盖libstdc++(由GCC和潜在的Clang使用)的抛出机制。 - ingomueller.net

70
如果您正在使用Boost 1.65或更高版本,则可以使用boost::stacktrace
#include <boost/stacktrace.hpp>

// ... somewhere inside the bar(int) function that is called recursively:
std::cout << boost::stacktrace::stacktrace();

9
Boost文档不仅解释了如何捕获堆栈跟踪,还解释了如何为异常和断言进行捕获。非常棒。 - moodboom
2
这个 stacktrace() 是否会按照 GettingStarted 指南中给出的源文件和行号进行打印? - Gimhani
1
即将到来的C++23 - https://en.cppreference.com/w/cpp/header/stacktrace - Dan

29

我想添加一个标准库选项(即跨平台)来生成异常回溯,这已经在C++11中提供:

使用 std::nested_exceptionstd::throw_with_nested

这不会提供堆栈展开,但在我看来是次优解。 在 这里这里 描述了如何在代码内获取异常回溯而无需调试器或笨重的日志记录,只需编写适当的异常处理程序即可重新抛出嵌套异常。

由于您可以对任何派生异常类执行此操作,因此可以向此类回溯中添加大量信息! 您还可以查看我的GitHub上的MWE,其中回溯将类似于:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

如果你愿意付出额外的努力,这可能比通常的愚蠢堆栈跟踪要好得多。 - Clearer
9
自从我意识到C++能把一个如此简单的概念弄得令人毛骨悚然以来,已经有一段时间了。感谢您提醒我。 - Martin

22

这并不能帮助你在异常被抛出时显示堆栈跟踪。 - undefined
1
在Windows上,你可以使用SetUnhandledExceptionFilter来实现这个功能。请注意,当从调试器中调用该函数时,它似乎不起作用。(参考链接) - undefined

10
如果您在使用C++并且不想/不能使用Boost,则可以使用以下代码[链接到原始网站]打印带有解码名称的回溯信息。
请注意,此解决方案特定于Linux。它使用GNU的libc函数backtrace()/backtrace_symbols()(来自execinfo.h)获取回溯信息,然后使用__cxa_demangle()(来自cxxabi.h)对回溯符号名称进行解码。
// stacktrace.h (c) 2008, Timo Bingmann from http://idlebox.net/
// published under the WTFPL v2.0

#ifndef _STACKTRACE_H_
#define _STACKTRACE_H_

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

/** Print a demangled stack backtrace of the caller function to FILE* out. */
static inline void print_stacktrace(FILE *out = stderr, unsigned int max_frames = 63)
{
    fprintf(out, "stack trace:\n");

    // storage array for stack trace address data
    void* addrlist[max_frames+1];

    // retrieve current stack addresses
    int addrlen = backtrace(addrlist, sizeof(addrlist) / sizeof(void*));

    if (addrlen == 0) {
    fprintf(out, "  <empty, possibly corrupt>\n");
    return;
    }

    // resolve addresses into strings containing "filename(function+address)",
    // this array must be free()-ed
    char** symbollist = backtrace_symbols(addrlist, addrlen);

    // allocate string which will be filled with the demangled function name
    size_t funcnamesize = 256;
    char* funcname = (char*)malloc(funcnamesize);

    // iterate over the returned symbol lines. skip the first, it is the
    // address of this function.
    for (int i = 1; i < addrlen; i++)
    {
    char *begin_name = 0, *begin_offset = 0, *end_offset = 0;

    // find parentheses and +address offset surrounding the mangled name:
    // ./module(function+0x15c) [0x8048a6d]
    for (char *p = symbollist[i]; *p; ++p)
    {
        if (*p == '(')
        begin_name = p;
        else if (*p == '+')
        begin_offset = p;
        else if (*p == ')' && begin_offset) {
        end_offset = p;
        break;
        }
    }

    if (begin_name && begin_offset && end_offset
        && begin_name < begin_offset)
    {
        *begin_name++ = '\0';
        *begin_offset++ = '\0';
        *end_offset = '\0';

        // mangled name is now in [begin_name, begin_offset) and caller
        // offset in [begin_offset, end_offset). now apply
        // __cxa_demangle():

        int status;
        char* ret = abi::__cxa_demangle(begin_name,
                        funcname, &funcnamesize, &status);
        if (status == 0) {
        funcname = ret; // use possibly realloc()-ed string
        fprintf(out, "  %s : %s+%s\n",
            symbollist[i], funcname, begin_offset);
        }
        else {
        // demangling failed. Output function name as a C function with
        // no arguments.
        fprintf(out, "  %s : %s()+%s\n",
            symbollist[i], begin_name, begin_offset);
        }
    }
    else
    {
        // couldn't parse the line? print the whole line.
        fprintf(out, "  %s\n", symbollist[i]);
    }
    }

    free(funcname);
    free(symbollist);
}

#endif // _STACKTRACE_H_

HTH!


6

由于在进入catch块时堆栈已经被解开,因此我在这种情况下的解决方案是不要捕获某些异常,这些异常会导致SIGABRT异常。在SIGABRT信号处理程序中,我使用fork()和execl()函数调用gdb(在调试构建中)或Google breakpads stackwalk(在发布构建中)。此外,我尝试仅使用信号处理程序安全的函数。

GDB:

static const char BACKTRACE_START[] = "<2>--- backtrace of entire stack ---\n";
static const char BACKTRACE_STOP[] = "<2>--- backtrace finished ---\n";

static char *ltrim(char *s)
{
    while (' ' == *s) {
        s++;
    }
    return s;
}

void Backtracer::print()
{
    int child_pid = ::fork();
    if (child_pid == 0) {
        // redirect stdout to stderr
        ::dup2(2, 1);

        // create buffer for parent pid (2+16+1 spaces to allow up to a 64 bit hex parent pid)
        char pid_buf[32];
        const char* stem = "                   ";
        const char* s = stem;
        char* d = &pid_buf[0];
        while (static_cast<bool>(*s))
        {
            *d++ = *s++;
        }
        *d-- = '\0';
        char* hexppid = d;

        // write parent pid to buffer and prefix with 0x
        int ppid = getppid();
        while (ppid != 0) {
            *hexppid = ((ppid & 0xF) + '0');
            if(*hexppid > '9') {
                *hexppid += 'a' - '0' - 10;
            }
            --hexppid;
            ppid >>= 4;
        }
        *hexppid-- = 'x';
        *hexppid = '0';

        // invoke GDB
        char name_buf[512];
        name_buf[::readlink("/proc/self/exe", &name_buf[0], 511)] = 0;
        ssize_t r = ::write(STDERR_FILENO, &BACKTRACE_START[0], sizeof(BACKTRACE_START));
        (void)r;
        ::execl("/usr/bin/gdb",
                "/usr/bin/gdb", "--batch", "-n", "-ex", "thread apply all bt full", "-ex", "quit",
                &name_buf[0], ltrim(&pid_buf[0]), nullptr);
        ::exit(1); // if GDB failed to start
    } else if (child_pid == -1) {
        ::exit(1); // if forking failed
    } else {
        // make it work for non root users
        if (0 != getuid()) {
            ::prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);
        }
        ::waitpid(child_pid, nullptr, 0);
        ssize_t r = ::write(STDERR_FILENO, &BACKTRACE_STOP[0], sizeof(BACKTRACE_STOP));
        (void)r;
    }
}

minidump_stackwalk:

static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded)
{
    int child_pid = ::fork();
    if (child_pid == 0) {
        ::dup2(open("/dev/null", O_WRONLY), 2); // ignore verbose output on stderr
        ssize_t r = ::write(STDOUT_FILENO, &MINIDUMP_STACKWALK_START[0], sizeof(MINIDUMP_STACKWALK_START));
        (void)r;
        ::execl("/usr/bin/minidump_stackwalk", "/usr/bin/minidump_stackwalk", descriptor.path(), "/usr/share/breakpad-syms", nullptr);
        ::exit(1); // if minidump_stackwalk failed to start
    } else if (child_pid == -1) {
        ::exit(1); // if forking failed
    } else {
        ::waitpid(child_pid, nullptr, 0);
        ssize_t r = ::write(STDOUT_FILENO, &MINIDUMP_STACKWALK_STOP[0], sizeof(MINIDUMP_STACKWALK_STOP));
        (void)r;
    }
    ::remove(descriptor.path()); // this is not signal safe anymore but should still work
    return succeeded;
}

编辑:为了使它能在breakpad中正常工作,我还需要添加以下内容:

std::set_terminate([]()
{
    ssize_t r = ::write(STDERR_FILENO, EXCEPTION, sizeof(EXCEPTION));
    (void)r;
    google_breakpad::ExceptionHandler::WriteMinidump(std::string("/tmp"), dumpCallback, NULL);
    exit(1); // avoid creating a second dump by not calling std::abort
});

来源: 如何使用带有行号信息的gcc获取C++的堆栈跟踪?是否可能将gdb附加到崩溃的进程(即“即时”调试)

这里提供两个相关的来源,可以帮助您在C ++中获取堆栈跟踪和调试崩溃的进程。使用带有行号信息的GCC编译器可以帮助您获取更精确的堆栈跟踪信息。同时,您还可以通过将GDB附加到崩溃的进程来进行“即时”调试并捕获堆栈跟踪信息。

6
据我所知,libunwind非常易于移植,到目前为止我还没有发现比它更容易使用的东西。

1
libunwind 1.1在OS X上无法构建。 - xaxxon

5

5
在它的主页上,我看到了throw stack_runtime_error。我推断这个库只适用于从那个类派生的异常,并且不适用于std::exception或来自第三方库的异常。 - Thomas

3

我有类似的问题,虽然我喜欢可移植性,但我只需要gcc支持。在gcc中,可以使用execinfo.h和backtrace调用。为了解开函数名,Bingmann先生有一段很好的代码。要在异常上转储回溯信息,我会创建一个构造函数来打印回溯信息。如果我希望它能够处理库中抛出的异常,可能需要重新构建/链接,以便使用回溯异常。

/******************************************
#Makefile with flags for printing backtrace with function names
# compile with symbols for backtrace
CXXFLAGS=-g
# add symbols to dynamic symbol table for backtrace
LDFLAGS=-rdynamic
turducken: turducken.cc
******************************************/

#include <cstdio>
#include <stdexcept>
#include <execinfo.h>
#include "stacktrace.h" /* https://panthema.net/2008/0901-stacktrace-demangled/ */

// simple exception that prints backtrace when constructed
class btoverflow_error: public std::overflow_error
{
    public:
    btoverflow_error( const std::string& arg ) :
        std::overflow_error( arg )
    {
        print_stacktrace();
    };
};


void chicken(void)
{
    throw btoverflow_error( "too big" );
}

void duck(void)
{
    chicken();
}

void turkey(void)
{
    duck();
}

int main( int argc, char *argv[])
{
    try
    {
        turkey();
    }
    catch( btoverflow_error e)
    {
        printf( "caught exception: %s\n", e.what() );
    }
}

使用gcc 4.8.4编译并运行,可以得到一个带有漂亮未混淆C++函数名称的回溯:
stack trace:
 ./turducken : btoverflow_error::btoverflow_error(std::string const&)+0x43
 ./turducken : chicken()+0x48
 ./turducken : duck()+0x9
 ./turducken : turkey()+0x9
 ./turducken : main()+0x15
 /lib/x86_64-linux-gnu/libc.so.6 : __libc_start_main()+0xf5
 ./turducken() [0x401629]

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