如何在C++中打印捕获异常的堆栈跟踪 & C++中的代码注入

29

我希望不仅能捕获我的异常的堆栈跟踪,还能捕获任何std::exception的后代的堆栈跟踪。

据我所知,当异常被捕获时,由于堆栈展开(卸载),堆栈跟踪完全丢失。

因此,我唯一看到的方法是在std::exception构造函数调用的位置注入代码以保存上下文信息(堆栈跟踪)。我对吗?

如果是这样,请告诉我如何在C++中实现代码注入(如果可以)。您的方法可能不完全安全,因为我只需要在我的应用程序的Debug版本中使用它。也许我需要使用汇编语言?

我只关心GCC的解决方案。它可以使用c++0x功能。


1
这个答案或许有帮助:https://dev59.com/5nA75IYBdhLWcg3wVXWv - jxh
@user315052 那个答案是针对未捕获的异常,对于已捕获的异常不起作用。 - boqapt
确实可以将 C 字符串的数组存入一个 std::string 中,并将其作为异常构造函数中的 what 参数(或者仅作为 what 的一部分)传递进去。 - jxh
我尝试编写一个宏,通过throw_with_nested将回溯附加到捕获的异常上,但遗憾的是,我的编译器缺乏对C++11的支持。 - jxh
是的,但您可以记录一个堆栈跟踪,靠近抛出异常的位置。您可以尽可能接近实际调用STL库代码。除非修改库代码,否则这是处理其他代码抛出的异常的最佳方法。 - jxh
显示剩余2条评论
5个回答

41

既然您提到您对GCC特定的内容感到满意,我已经为您准备了一个示例,展示了一种可能的实现方式。但是这种方式纯粹是邪恶的,会侵犯C++支持库的内部。我不确定我是否想在生产代码中使用这个。无论如何:

#include <iostream>
#include <dlfcn.h>
#include <execinfo.h>
#include <typeinfo>
#include <string>
#include <memory>
#include <cxxabi.h>
#include <cstdlib>

namespace {
  void * last_frames[20];
  size_t last_size;
  std::string exception_name;

  std::string demangle(const char *name) {
    int status;
    std::unique_ptr<char,void(*)(void*)> realname(abi::__cxa_demangle(name, 0, 0, &status), &std::free);
    return status ? "failed" : &*realname;
  }
}

extern "C" {
  void __cxa_throw(void *ex, void *info, void (*dest)(void *)) {
    exception_name = demangle(reinterpret_cast<const std::type_info*>(info)->name());
    last_size = backtrace(last_frames, sizeof last_frames/sizeof(void*));

    static void (*const rethrow)(void*,void*,void(*)(void*)) __attribute__ ((noreturn)) = (void (*)(void*,void*,void(*)(void*)))dlsym(RTLD_NEXT, "__cxa_throw");
    rethrow(ex,info,dest);
  }
}

void foo() {
  throw 0;
}

int main() {
  try {
    foo();
  }
  catch (...) {
    std::cerr << "Caught a: " << exception_name << std::endl;
    // print to stderr
    backtrace_symbols_fd(last_frames, last_size, 2);
  }
}
我们基本上是窃取了GCC用于分派抛出异常的内部实现函数。在那一点上,我们获取堆栈跟踪并将其保存在全局变量中。然后当我们稍后在try/catch中遇到该异常时,我们可以使用堆栈跟踪来打印/保存或者你想做什么其他操作。我们使用dlsym()来查找__cxa_throw的真实版本。
我的示例抛出一个int来证明你可以使用任何类型来实现这个,不仅限于用户定义的异常。
它使用type_info来获取被抛出的类型名称,然后对其进行解码。
如果需要,您可以更好地封装存储堆栈跟踪的全局变量。
我使用以下编译和测试的:
g++ -Wall -Wextra test.cc -g -O0 -rdynamic -ldl
运行时输出如下:
./a.out Caught a: int ./a.out(__cxa_throw+0x74)[0x80499be] ./a.out(main+0x0)[0x8049a61] ./a.out(main+0x10)[0x8049a71] /lib/i686/cmov/libc.so.6(__libc_start_main+0xe6)[0xb75c2ca6] ./a.out[0x80497e1]
请不要将此视为良好建议的示例 - 它只是展示了一些小技巧和探索内部的例子!

@user484936 - 完成了。它现在使用C++11的unique_ptr。我还没有完全弄清楚如何将[[noreturn]]放入函数指针中,所以使用C++11编译会添加一个(虚假的)警告。 - Flexo
这个hack现在已经成为我的库的一部分:http://code.google.com/p/libglim/source/browse/trunk/exception.hpp - ArtemGr
@user484936,你提到的页面已经不存在了,但可以在https://web.archive.org/web/20150706050621/http://blog.sjinks.pro/c-cpp/969-track-uncaught-exceptions找到。不幸的是,它看起来更加复杂,而且是俄语! - Shital Shah
1
我会在你的__cxa_throw函数中添加一个if语句,检查全局或线程本地标志。 - Flexo
1
你可以使用 boost.stacktrace? 使堆栈跟踪的打印更加便携(并且更详细)。 - ingomueller.net
显示剩余5条评论

5
在Linux中,这可以通过在异常构造函数中添加调用backtrace()来将堆栈跟踪捕获到异常的成员变量中来实现。不幸的是,这对于标准异常不起作用,只适用于您定义的异常。

3
几年前我写了这篇文章:C++中的解除chained exceptions。基本上,一些宏在抛出异常时记录堆栈展开发生的位置。
该框架的更新版本可以在库Imebra(http://imebra.com)中找到。
我将重新实现其中的某些部分(例如,在线程本地存储中存储堆栈跟踪)。

2
Flexo的解决方案非常好,效果也很好。它有一个好处,即从回溯地址到过程名称的转换仅在catch部分执行,因此是否关心回溯是异常接收者的责任。
然而,在某些情况下,基于libunwind的解决方案可能更可取,例如因为在某些情况下libunwind可以收集过程名,而backtrace函数却无法这样做。
在这里,我提出了一种基于Flexo答案的想法,但进行了多个扩展。它使用libunwind在抛出时生成回溯,并直接打印到stderr。它使用libDL来识别共享对象文件名。它使用elfutils的DWARF调试信息收集源代码文件名和行号。它使用C++ API解构C++异常。用户可以设置mExceptionStackTrace变量以临时启用/禁用堆栈跟踪。
关于拦截__cxa_throw的所有解决方案的一个重要点是,它们增加了遍历堆栈的潜在开销。我的解决方案特别适用于访问调试器符号以收集源文件名而添加了显着的开销。这在自动测试中可能是可以接受的,因为您希望代码不会引发异常,并且您希望对引发异常的(失败的)测试有一个强大的堆栈跟踪。
// Our stack unwinding is a GNU C extension:
#if defined(__GNUC__)
// include elfutils to parse debugger information:
#include <elfutils/libdwfl.h>

// include libunwind to gather the stack trace:
#define UNW_LOCAL_ONLY
#include <libunwind.h>

#include <dlfcn.h>
#include <cxxabi.h>
#include <typeinfo>
#include <stdio.h>
#include <stdlib.h>

#define LIBUNWIND_MAX_PROCNAME_LENGTH 4096

static bool mExceptionStackTrace = false;


// We would like to print a stacktrace for every throw (even in
// sub-libraries and independent of the object thrown). This works
// only for gcc and only with a bit of trickery
extern "C" {
    void print_exception_info(const std::type_info* aExceptionInfo) {
        int vDemangleStatus;
        char* vDemangledExceptionName;

        if (aExceptionInfo != NULL) {
            // Demangle the name of the exception using the GNU C++ ABI:
            vDemangledExceptionName = abi::__cxa_demangle(aExceptionInfo->name(), NULL, NULL, &vDemangleStatus);
            if (vDemangledExceptionName != NULL) {
                fprintf(stderr, "\n");
                fprintf(stderr, "Caught exception %s:\n", vDemangledExceptionName);

                // Free the memory from __cxa_demangle():
                free(vDemangledExceptionName);
            } else {
                // NOTE: if the demangle fails, we do nothing, so the
                // non-demangled name will be printed. Thats ok.
                fprintf(stderr, "\n");
                fprintf(stderr, "Caught exception %s:\n", aExceptionInfo->name());
            }
        } else {
            fprintf(stderr, "\n");
            fprintf(stderr, "Caught exception:\n");
        }
    }

    void libunwind_print_backtrace(const int aFramesToIgnore) {
        unw_cursor_t vUnwindCursor;
        unw_context_t vUnwindContext;
        unw_word_t ip, sp, off;
        unw_proc_info_t pip;
        int vUnwindStatus, vDemangleStatus, i, n = 0;
        char vProcedureName[LIBUNWIND_MAX_PROCNAME_LENGTH];
        char* vDemangledProcedureName;
        const char* vDynObjectFileName;
        const char* vSourceFileName;
        int vSourceFileLineNumber;

        // This is from libDL used for identification of the object file names:
        Dl_info dlinfo;

        // This is from DWARF for accessing the debugger information:
        Dwarf_Addr addr;
        char* debuginfo_path = NULL;
        Dwfl_Callbacks callbacks = {};
        Dwfl_Line* vDWARFObjLine;


        // initialize the DWARF handling:
        callbacks.find_elf = dwfl_linux_proc_find_elf;
        callbacks.find_debuginfo = dwfl_standard_find_debuginfo;
        callbacks.debuginfo_path = &debuginfo_path;
        Dwfl* dwfl = dwfl_begin(&callbacks);
        if (dwfl == NULL) {
            fprintf(stderr, "libunwind_print_backtrace(): Error initializing DWARF.\n");
        }
        if ((dwfl != NULL) && (dwfl_linux_proc_report(dwfl, getpid()) != 0)) {
            fprintf(stderr, "libunwind_print_backtrace(): Error initializing DWARF.\n");
            dwfl = NULL;
        }
        if ((dwfl != NULL) && (dwfl_report_end(dwfl, NULL, NULL) != 0)) {
            fprintf(stderr, "libunwind_print_backtrace(): Error initializing DWARF.\n");
            dwfl = NULL;
        }


        // Begin stack unwinding with libunwnd:
        vUnwindStatus = unw_getcontext(&vUnwindContext);
        if (vUnwindStatus) {
            fprintf(stderr, "libunwind_print_backtrace(): Error in unw_getcontext: %d\n", vUnwindStatus);
            return;
        }

        vUnwindStatus = unw_init_local(&vUnwindCursor, &vUnwindContext);
        if (vUnwindStatus) {
            fprintf(stderr, "libunwind_print_backtrace(): Error in unw_init_local: %d\n", vUnwindStatus);
            return;
        }

        vUnwindStatus = unw_step(&vUnwindCursor);
        for (i = 0; ((i < aFramesToIgnore) && (vUnwindStatus > 0)); ++i) {
            // We ignore the first aFramesToIgnore stack frames:
            vUnwindStatus = unw_step(&vUnwindCursor);
        }


        while (vUnwindStatus > 0) {
            pip.unwind_info = NULL;
            vUnwindStatus = unw_get_proc_info(&vUnwindCursor, &pip);
            if (vUnwindStatus) {
                fprintf(stderr, "libunwind_print_backtrace(): Error in unw_get_proc_info: %d\n", vUnwindStatus);
                break;
            }

            // Resolve the address of the stack frame using libunwind:
            unw_get_reg(&vUnwindCursor, UNW_REG_IP, &ip);
            unw_get_reg(&vUnwindCursor, UNW_REG_SP, &sp);

            // Resolve the name of the procedure using libunwind:
            // unw_get_proc_name() returns 0 on success, and returns UNW_ENOMEM
            // if the procedure name is too long to fit in the buffer provided and
            // a truncated version of the name has been returned:
            vUnwindStatus = unw_get_proc_name(&vUnwindCursor, vProcedureName, LIBUNWIND_MAX_PROCNAME_LENGTH, &off);
            if (vUnwindStatus == 0) {
                // Demangle the name of the procedure using the GNU C++ ABI:
                vDemangledProcedureName = abi::__cxa_demangle(vProcedureName, NULL, NULL, &vDemangleStatus);
                if (vDemangledProcedureName != NULL) {
                    strncpy(vProcedureName, vDemangledProcedureName, LIBUNWIND_MAX_PROCNAME_LENGTH);
                    // Free the memory from __cxa_demangle():
                    free(vDemangledProcedureName);
                } else {
                    // NOTE: if the demangle fails, we do nothing, so the
                    // non-demangled name will be printed. Thats ok.
                }
            } else if (vUnwindStatus == UNW_ENOMEM) {
                // NOTE: libunwind could resolve the name, but could not store
                // it in a buffer of only LIBUNWIND_MAX_PROCNAME_LENGTH characters.
                // So we have a truncated procedure name that can not be demangled.
                // We ignore the problem and the truncated non-demangled name will
                // be printed.
            } else {
                vProcedureName[0] = '?';
                vProcedureName[1] = '?';
                vProcedureName[2] = '?';
                vProcedureName[3] = 0;
            }


            // Resolve the object file name using dladdr:
            if (dladdr((void *)(pip.start_ip + off), &dlinfo) && dlinfo.dli_fname && *dlinfo.dli_fname) {
                vDynObjectFileName = dlinfo.dli_fname;
            } else {
                vDynObjectFileName = "???";
            }


            // Resolve the source file name using DWARF:
            if (dwfl != NULL) {
                addr = (uintptr_t)(ip - 4);
                Dwfl_Module* module = dwfl_addrmodule(dwfl, addr);
                // Here we could also ask for the procedure name:
                //const char* vProcedureName = dwfl_module_addrname(module, addr);
                // Here we could also ask for the object file name:
                //vDynObjectFileName = dwfl_module_info(module, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
                vDWARFObjLine = dwfl_getsrc(dwfl, addr);
                if (vDWARFObjLine != NULL) {
                    vSourceFileName = dwfl_lineinfo(vDWARFObjLine, &addr, &vSourceFileLineNumber, NULL, NULL, NULL);
                    //fprintf(stderr, " %s:%d", strrchr(vSourceFileName, '/')+1, vSourceFileLineNumber);
                }
            }
            if (dwfl == NULL || vDWARFObjLine == NULL || vSourceFileName == NULL) {
                vSourceFileName = "???";
                vSourceFileLineNumber = 0;
            }


            // Print the stack frame number:
            fprintf(stderr, "#%2d:", ++n);

            // Print the stack addresses:
            fprintf(stderr, " 0x%016" PRIxPTR " sp=0x%016" PRIxPTR, static_cast<uintptr_t>(ip), static_cast<uintptr_t>(sp));

            // Print the source file name:
            fprintf(stderr, " %s:%d", vSourceFileName, vSourceFileLineNumber);

            // Print the dynamic object file name (that is the library name).
            // This is typically not interesting if we have the source file name.
            //fprintf(stderr, " %s", vDynObjectFileName);

            // Print the procedure name:
            fprintf(stderr, " %s", vProcedureName);

            // Print the procedure offset:
            //fprintf(stderr, " + 0x%" PRIxPTR, static_cast<uintptr_t>(off));

            // Print a newline to terminate the output:
            fprintf(stderr, "\n");


            // Stop the stack trace at the main method (there are some
            // uninteresting higher level functions on the stack):
            if (strcmp(vProcedureName, "main") == 0) {
                break;
            }

            vUnwindStatus = unw_step(&vUnwindCursor);
            if (vUnwindStatus < 0) {
                fprintf(stderr, "libunwind_print_backtrace(): Error in unw_step: %d\n", vUnwindStatus);
            }
        }
    }

    void __cxa_throw(void *thrown_exception, std::type_info *info, void (*dest)(void *)) {
        // print the stack trace to stderr:
        if (mExceptionStackTrace) {
            print_exception_info(info);
            libunwind_print_backtrace(1);
        }

        // call the real __cxa_throw():
        static void (*const rethrow)(void*,void*,void(*)(void*)) __attribute__ ((noreturn)) = (void (*)(void*,void*,void(*)(void*)))dlsym(RTLD_NEXT, "__cxa_throw");
        rethrow(thrown_exception,info,dest);
    }
}
#endif

0

看看backward-cpp,它做得很好并且被精心维护

示例代码

在trace.hxx中

#define BACKWARD_HAS_DW 1 // or #define BACKWARD_HAS_BFD 1 check docs
#include <backward.hpp>

class recoverable_err final: std::runtime_error
  {
    backward::StackTrace stacktrace_;

  public:
    explicit recoverable_err(std::string msg) noexcept;

    auto
    print_stacktrace(std::ostream &stream)const noexcept -> void;

    [[nodiscard]] auto
    what() const noexcept -> const char * final;
  };

在 trace.cxx 中

  #include "trace.hxx"
  
  recoverable_err::recoverable_err(std::string msg) noexcept
      : std::runtime_error{ msg }
      , stacktrace_{ backward::StackTrace() }
  {
    stacktrace_.load_here();
  }

  auto
  recoverable_err::print_stacktrace(std::ostream &stream)const  noexcept -> void
  {
    using namespace backward;
    Printer p;
    p.object = true;
    p.color_mode = ColorMode::always;
    p.address = true;
    p.print(stacktrace_, stream);
  }

  auto
  recoverable_err::what() const noexcept -> const char *
  {
    return std::runtime_error::what();
  }

在主函数中的使用

auto
main() -> int
{
  try
    {
      throw recoverable_err("Recover from nasty error");
    }
  catch (recoverable_err const &ex)
    {
      std::cerr << ex.what();
      ex.print_stacktrace(std::cerr);
    }
  catch (std::exception const &ex)
    {
      std::cerr << "Using default class\n";
      std::cerr << ex.what();
    }
}

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