我如何找到C ++中引发异常的位置?


92

我有一个程序会在某个地方抛出未捕获的异常。我所得到的只是关于引发异常的报告,而没有关于引发异常的信息。对于编译为包含调试符号而不通知我代码中生成异常的地方的程序,这似乎是不合逻辑的。

有什么办法可以说明我的异常来自哪里,因为没有在gdb中设置“ catch throw”并为每个抛出的异常调用回溯?



捕获异常并查看内部消息是什么。既然是很好的做法,从标准的例外之一(STD :: runtime_error)得出一个例外,你shoudl能够与捕捉住它(标准::异常const的&E)
马丁纽约

1
而std :: exception / Std :: runtime_error解决了找出异常的“路径”和来源的问题?
VolkerK 2010年

1
作为您的问题状态gdb,我认为您的解决方案已经在SO中: stackoverflow.com/questions/77005/… 我已经使用了此处描述的解决方案,并且效果很好。
神经

2
您应该考虑通过标签指定操作系统。由于您提到了gdb,因此我假设您正在寻找的是Linux解决方案,而不是Windows。
jschmier 2010年

Answers:


72

这是一些可能的信息用于调试问题的信息

如果未捕获到异常,std::terminate()则会自动调用特殊的库函数。Terminate实际上是指向函数的指针,默认值为Standard C库函数std::abort()。如果没有清理发生了未捕获的异常,它可能实际上是因为没有析构函数调用调试这个问题有帮助。
†由实现定义,在std::terminate()调用之前是否将堆栈退卷。


调用abort()通常对生成核心转储很有用,可以对其进行分析以确定异常原因。确保通过ulimit -c unlimited(Linux)启用核心转储。


您可以使用安装自己的terminate()功能std::set_terminate()。您应该能够在gdb中的终止函数上设置断点。您可能能够从terminate()函数中生成堆栈回溯,并且此回溯可能有助于识别异常的位置。

有一个简短的讨论未捕获的异常布鲁斯Eckel的C ++中的思考,第二版可能有帮助。


由于默认情况下会进行terminate()调用abort()(默认情况下会产生SIGABRT信号),因此您可以设置SIGABRT处理程序,然后从signal handler中打印堆栈回溯。此回溯可能有助于识别异常的位置。


注意:我说可能是因为通过使用语言结构,以单独的错误处理处理C ++支持非本地的错误,并从普通代码报告代码。捕获块可以并且经常位于与投掷点不同的功能/方法中。在注释中(感谢Dan)也向我指出,terminate()调用之前是否取消堆栈是实现定义的。

更新:我整理了一个名为Linux的测试程序,该程序在terminate()通过set_terminate()和的信号处理程序中的函数集中生成了回溯SIGABRT。这两个回溯都正确显示了未处理异常的位置。

更新2:多亏了一篇关于在terminate中捕获未捕获的异常的博客文章,我学到了一些新的技巧。包括在终止处理程序中重新抛出未捕获的异常。重要的是要注意,throw自定义终止处理程序中的空语句可用于GCC,而不是可移植的解决方案。

码:

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#ifndef __USE_GNU
#define __USE_GNU
#endif

#include <execinfo.h>
#include <signal.h>
#include <string.h>

#include <iostream>
#include <cstdlib>
#include <stdexcept>

void my_terminate(void);

namespace {
    // invoke set_terminate as part of global constant initialization
    static const bool SET_TERMINATE = std::set_terminate(my_terminate);
}

// This structure mirrors the one found in /usr/include/asm/ucontext.h
typedef struct _sig_ucontext {
   unsigned long     uc_flags;
   struct ucontext   *uc_link;
   stack_t           uc_stack;
   struct sigcontext uc_mcontext;
   sigset_t          uc_sigmask;
} sig_ucontext_t;

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

    // Get the address at the time the signal was raised from the EIP (x86)
    void * caller_address = (void *) uc->uc_mcontext.eip;
    
    std::cerr << "signal " << sig_num 
              << " (" << strsignal(sig_num) << "), address is " 
              << info->si_addr << " from " 
              << caller_address << std::endl;

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

    std::cerr << __FUNCTION__ << " backtrace returned " 
              << size << " frames\n\n";

    // overwrite sigaction with caller's address
    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) {
        std::cerr << "[bt]: (" << i << ") " << messages[i] << std::endl;
    }
    std::cerr << std::endl;

    free(messages);

    exit(EXIT_FAILURE);
}

void my_terminate() {
    static bool tried_throw = false;

    try {
        // try once to re-throw currently active exception
        if (!tried_throw++) throw;
    }
    catch (const std::exception &e) {
        std::cerr << __FUNCTION__ << " caught unhandled exception. what(): "
                  << e.what() << std::endl;
    }
    catch (...) {
        std::cerr << __FUNCTION__ << " caught unknown/unhandled exception." 
                  << std::endl;
    }

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

    std::cerr << __FUNCTION__ << " backtrace returned " 
              << size << " frames\n\n";

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

    for (int i = 0; i < size && messages != NULL; ++i) {
        std::cerr << "[bt]: (" << i << ") " << messages[i] << std::endl;
    }
    std::cerr << std::endl;

    free(messages);

    abort();
}

int throw_exception() {
    // throw an unhandled runtime error
    throw std::runtime_error("RUNTIME ERROR!");
    return 0;
}

int foo2() {
    throw_exception();
    return 0;
}

int foo1() {
    foo2();
    return 0;
}

int main(int argc, char ** argv) {
    struct sigaction sigact;

    sigact.sa_sigaction = crit_err_hdlr;
    sigact.sa_flags = SA_RESTART | SA_SIGINFO;

    if (sigaction(SIGABRT, &sigact, (struct sigaction *)NULL) != 0) {
        std::cerr << "error setting handler for signal " << SIGABRT 
                  << " (" << strsignal(SIGABRT) << ")\n";
        exit(EXIT_FAILURE);
    }

    foo1();

    exit(EXIT_SUCCESS);
}

输出:

my_terminate捕获了未处理的异常。what():运行时错误!
my_terminate backtrace返回10帧

[bt] :( 0)。/test(my_terminate__Fv + 0x1a)[0x8048e52]
[bt] :( 1)/usr/lib/libstdc++-libc6.2-2.so.3 [0x40045baa]
[bt]:(2)/usr/lib/libstdc++-libc6.2-2.so.3 [0x400468e5]
[bt] :( 3)/usr/lib/libstdc++-libc6.2-2.so.3(__rethrow+0xaf)[0x40046bdf]
[bt] :( 4)。/test(throw_exception__Fv + 0x68)[0x8049008]
[bt] :( 5)。/test(foo2__Fv + 0xb)[0x8049043]
[bt] :( 6)。/test(foo1__Fv + 0xb)[0x8049057]
[bt] :( 7)。/test(main + 0xc1)[0x8049121]
[bt] :( 8)。/test(__ libc_start_main + 0x95)[0x42017589]
[bt] :( 9)。/test(__ eh_alloc + 0x3d)[0x8048b21]

信号6(已终止),地址是0x42029331的0x1239
crit_err_hdlr backtrace返回了13帧

[bt] :( 1)。/test(kill + 0x11)[0x42029331]
[bt] :( 2)./test(abort+0x16e)[0x4202a8c2]
[bt] :( 3)个。/test[0x8048f9f]
[bt] :( 4)/usr/lib/libstdc++-libc6.2-2.so.3 [0x40045baa]
[bt] :( 5)/usr/lib/libstdc++-libc6.2-2.so.3 [0x400468e5]
[bt] :( 6)/usr/lib/libstdc++-libc6.2-2.so.3(__rethrow+0xaf)[0x40046bdf]
[bt] :( 7)。/test(throw_exception__Fv + 0x68)[0x8049008]
[bt] :( 8)。/test(foo2__Fv + 0xb)[0x8049043]
[bt] :( 9)。/test(foo1__Fv + 0xb)[0x8049057]
[bt] :( 10)。/test(main + 0xc1)[0x8049121]
[bt] :( 11)。/test(__ libc_start_main + 0x95)[0x42017589]
[bt] :( 12)。/test(__ eh_alloc + 0x3d)[0x8048b21]


1
很有意思。我一直怀疑一个未处理的异常会解开堆栈,直到它到达顶层(main),然后才会调用terminate()。但是您的示例显示完全不执行放松操作,这很酷。

6
1)throw(int)规格是不必要的。2)uc->uc_mcontext.eip可能非常依赖平台(例如,...rip在64位平台上使用)。3)进行编译,-rdynamic以便获得回溯符号。4)运行./a.out 2>&1 | c++filt以获得漂亮的回溯符号。

2
“没有为未捕获的异常进行清理。” -实际上,这是实现定义的。请参阅C ++规范中的15.3 / 9和15.5.1 / 2。“在没有找到匹配处理程序的情况下,实现是由实现定义的,在调用terminate()之前是否取消堆栈的堆栈。” 但是,如果您的编译器支持的话,这是一个不错的解决方案!

1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;为我的ARM目标工作
斯蒂芬,2015年

1
一些注意事项:backtrace_symbols()进行malloc ...因此,您可能希望在启动时预先分配一个内存块,然后在碰巧调用backtrace_symbols()之前在my_terminate()中对其进行分配。处理一个std :: bad_alloc()异常。。另外,还可以包括<cxxabi.h>然后使用__cxa_demangle(),以使错位串的一些有用的东西出输出消息[]串之间“(”和“+”显示
ķ斯科特彼尔

51

如您所说,我们可以在gdb中使用“ catch throw”,并为每个抛出的异常调用“ backtrace”。虽然这通常太麻烦而无法手动完成,但gdb允许该过程自动化。这样可以查看所有引发的异常的回溯,包括最后一个未捕获的异常:

gdb>

set pagination off
catch throw
commands
backtrace
continue
end
run

如果没有进一步的手动干预,则会生成大量回溯,包括最后一个未捕获的异常的回溯:

Catchpoint 1 (exception thrown), 0x00a30 in __cxa_throw () from libstdc++.so.6
#0  0x0da30 in __cxa_throw () from /usr/.../libstdc++.so.6
#1  0x021f2 in std::__throw_bad_weak_ptr () at .../shared_ptr_base.h:76
[...]
terminate called after throwing an instance of 'std::bad_weak_ptr'
  what():  bad_weak_ptr
Program received signal SIGABRT, Aborted.

这是一篇很棒的博客文章,总结了一下:http: //741mhz.com/throw-stacktrace [在archive.org上]


17

您可以创建如下宏:

#define THROW(exceptionClass, message) throw exceptionClass(__FILE__, __LINE__, (message) )

...并且它将为您提供引发异常的位置(当然不是堆栈跟踪)。您必须从采用上述构造函数的某些基类派生异常。


18
-1您不知道,throw new excation(...)throw exception(...)C ++不是Java,
Artyom

7
好吧,我修好了。原谅可能同时使用Java和C ++的程序员?
Erik Hermansen 2010年

虽然我已经用过了。它的问题是,它不能告诉实际引发异常的原因。例如,如果在try块中有5个stoi调用,您将不知道实际上是哪个罪魁祸首。
Banjocat 2015年

5

您没有传递有关使用哪种操作系统/编译器的信息。

在Visual Studio C ++中,可以检测到异常。

请参阅 ddj.com上的“ Visual C ++异常处理规范”

ddj.com上的我的文章 Postmortem Debugging”也包括使用Win32结构化异常处理(由工具使用)进行日志记录的代码。


他说gdb,这几乎排除了Windows / Visual Studio。
Ben Voigt 2010年

2
好吧,他说他想要“缺少gdb”的东西,但是他没有明确地指代任何OS / Compiler。多数民众赞成在没有声明这种东西的问题。
RED SOFT ADAIR,2010年

5

您可以将代码中的主要位置标记为noexcept定位异常,然后使用libunwind(只需添加-lunwind到链接器参数)(已通过测试clang++ 3.6):

demagle.hpp:

#pragma once

char const *
get_demangled_name(char const * const symbol) noexcept;

demangle.cpp:

#include "demangle.hpp"

#include <memory>

#include <cstdlib>

#include <cxxabi.h>

namespace
{

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wglobal-constructors"
#pragma clang diagnostic ignored "-Wexit-time-destructors"
std::unique_ptr< char, decltype(std::free) & > demangled_name{nullptr, std::free};
#pragma clang diagnostic pop

}

char const *
get_demangled_name(char const * const symbol) noexcept
{
    if (!symbol) {
        return "<null>";
    }
    int status = -4;
    demangled_name.reset(abi::__cxa_demangle(symbol, demangled_name.release(), nullptr, &status));
    return ((status == 0) ? demangled_name.get() : symbol);
}

backtrace.hpp:

#pragma once
#include <ostream>

void
backtrace(std::ostream & _out) noexcept;

backtrace.cpp:

#include "backtrace.hpp"

#include <iostream>
#include <iomanip>
#include <limits>
#include <ostream>

#include <cstdint>

#define UNW_LOCAL_ONLY
#include <libunwind.h>

namespace
{

void
print_reg(std::ostream & _out, unw_word_t reg) noexcept
{
    constexpr std::size_t address_width = std::numeric_limits< std::uintptr_t >::digits / 4;
    _out << "0x" << std::setfill('0') << std::setw(address_width) << reg;
}

char symbol[1024];

}

void
backtrace(std::ostream & _out) noexcept
{
    unw_cursor_t cursor;
    unw_context_t context;
    unw_getcontext(&context);
    unw_init_local(&cursor, &context);
    _out << std::hex << std::uppercase;
    while (0 < unw_step(&cursor)) {
        unw_word_t ip = 0;
        unw_get_reg(&cursor, UNW_REG_IP, &ip);
        if (ip == 0) {
            break;
        }
        unw_word_t sp = 0;
        unw_get_reg(&cursor, UNW_REG_SP, &sp);
        print_reg(_out, ip);
        _out << ": (SP:";
        print_reg(_out, sp);
        _out << ") ";
        unw_word_t offset = 0;
        if (unw_get_proc_name(&cursor, symbol, sizeof(symbol), &offset) == 0) {
            _out << "(" << get_demangled_name(symbol) << " + 0x" << offset << ")\n\n";
        } else {
            _out << "-- error: unable to obtain symbol name for this frame\n\n";
        }
    }
    _out << std::flush;
}

backtrace_on_terminate.hpp:

#include "demangle.hpp"
#include "backtrace.hpp"

#include <iostream>
#include <type_traits>
#include <exception>
#include <memory>
#include <typeinfo>

#include <cstdlib>

#include <cxxabi.h>

namespace
{

[[noreturn]]
void
backtrace_on_terminate() noexcept;

static_assert(std::is_same< std::terminate_handler, decltype(&backtrace_on_terminate) >{});

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wglobal-constructors"
#pragma clang diagnostic ignored "-Wexit-time-destructors"
std::unique_ptr< std::remove_pointer_t< std::terminate_handler >, decltype(std::set_terminate) & > terminate_handler{std::set_terminate(backtrace_on_terminate), std::set_terminate};
#pragma clang diagnostic pop

[[noreturn]]
void
backtrace_on_terminate() noexcept
{
    std::set_terminate(terminate_handler.release()); // to avoid infinite looping if any
    backtrace(std::clog);
    if (std::exception_ptr ep = std::current_exception()) {
        try {
            std::rethrow_exception(ep);
        } catch (std::exception const & e) {
            std::clog << "backtrace: unhandled exception std::exception:what(): " << e.what() << std::endl;
        } catch (...) {
            if (std::type_info * et = abi::__cxa_current_exception_type()) {
                std::clog << "backtrace: unhandled exception type: " << get_demangled_name(et->name()) << std::endl;
            } else {
                std::clog << "backtrace: unhandled unknown exception" << std::endl;
            }
        }
    }
    std::_Exit(EXIT_FAILURE); // change to desired return code
}

}

关于这个问题有好文章


1

我有在Windows / Visual Studio中执行此操作的代码,如果需要轮廓,请告诉我。不过,尽管不知道如何为dwarf2代码做这件事,一个快速的Google建议在libgcc中有一个函数_Unwind_Backtrace可能是您需要的一部分。


可能是因为“让我知道是否需要轮廓”不是一个有用的答案。但是_Unwind_Backtrace是; 补偿。
Thomas

基于OP提到gdb,我猜想Windows与之无关。亚历克斯当然可以自由地编辑他的问题以说Windows。
Ben Voigt 2010年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.