如何打印 C++ 中捕获的异常的堆栈跟踪和 C++ 中的代码注入
Posted
技术标签:
【中文标题】如何打印 C++ 中捕获的异常的堆栈跟踪和 C++ 中的代码注入【英文标题】:How can I print stack trace for caught exceptions in C++ & code injection in C++ 【发布时间】:2012-07-24 20:02:16 【问题描述】:我希望堆栈跟踪不仅适用于我的异常,还适用于 std::exception
的任何后代
据我了解,由于堆栈展开(展开)而捕获异常时,堆栈跟踪将完全丢失。
所以我看到获取它的唯一方法是在 std::exception
构造函数调用的位置注入代码保存上下文信息(堆栈跟踪)。我说的对吗?
如果是这样,请告诉我如何在 C++ 中进行代码注入(如果可以的话)。您的方法可能并不完全安全,因为我只需要它用于我的应用程序的调试版本。我可能需要使用汇编程序吗?
我只对 GCC 的解决方案感兴趣。它可以使用 c++0x 特性
【问题讨论】:
This answer 可能会有所帮助。 @user315052 该答案适用于未捕获的异常,不适用于已捕获的异常。 是的,但是您可以将 C 字符串数组填充到std::string
中,并将其作为 what
传递给异常的构造函数(或者它的很大一部分,无论如何)。
我尝试编写一个宏,通过throw_with_nested
将回溯附加到捕获的异常,但是可惜我的编译器中缺少 C++11 支持。
@user315052 捕获异常后,堆栈状态已经丢失,您无能为力
【参考方案1】:
在 Linux 上,这可以通过在异常构造函数中添加对 backtrace()
的调用来实现,以将堆栈跟踪捕获到异常的成员变量中。不幸的是,它不适用于标准异常,仅适用于您定义的异常。
【讨论】:
【参考方案2】:几年前我写过这个:Unchaining chained exceptions in C++
基本上,一些宏会记录抛出异常时堆栈展开的位置。
可以在库 Imebra (http://imebra.com) 中找到该框架的更新版本。
我会重新实现它的某些部分(例如将堆栈跟踪存储在线程本地存储中)。
【讨论】:
【参考方案3】:既然您提到您对 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 抓到一个: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 这里最大的风险是,当 ABI 发生变化时您没有注意到,最终陷入了未定义的行为痛苦世界。如果您有兴趣,尽管我可以扩展它以打印异常的类型,即使在 catch(...) 块中也是如此。 是的,我有兴趣,会很棒 我可能会在您的__cxa_throw
中添加一个 if 来检查全局或线程本地标志。
您可以使用boost.stacktrace?
使堆栈跟踪的打印更便携(更详细)【参考方案4】:
Flexo 的解决方案非常好,而且效果很好。它还有一个好处是,从回溯地址到过程名称的转换只在catch
部分执行,因此它取决于异常的接收者是否关心回溯。
但是,在某些情况下,可以首选基于 libunwind 的解决方案,即因为 libunwind 在某些情况下可以收集 backtrace
函数无法收集的过程名称。
在这里,我根据 Flexo 的回答提出了一个想法,但有几个扩展。它使用 libunwind 在 throw 时生成回溯,并直接打印到 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
【讨论】:
【参考方案5】:在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();
【讨论】:
以上是关于如何打印 C++ 中捕获的异常的堆栈跟踪和 C++ 中的代码注入的主要内容,如果未能解决你的问题,请参考以下文章