Wie finde ich heraus, wo in C ++ eine Ausnahme ausgelöst wurde?

91

Ich habe ein Programm, das irgendwo eine nicht erfasste Ausnahme auslöst. Ich erhalte nur einen Bericht über eine ausgelöste Ausnahme und keine Informationen darüber, wo sie ausgelöst wurde. Es erscheint unlogisch, wenn ein Programm, das Debug-Symbole enthält, mich nicht darüber informiert, wo in meinem Code eine Ausnahme generiert wurde.

Gibt es eine Möglichkeit zu erkennen, woher meine Ausnahmen kommen, wenn ich nicht 'catch throw' in gdb gesetzt und für jede einzelne ausgelöste Ausnahme eine Rückverfolgung aufgerufen habe?

Alex
quelle
Fangen Sie die Ausnahme ab und sehen Sie, was die interne Nachricht ist. Da es eine gute Praxis ist, eine Ausnahme von einer der Standardausnahmen (std :: runtime_error) abzuleiten, sollten Sie sie mit catch (std :: exception const & e) abfangen können
Martin York
1
Und std :: exception / Std :: runtime_error löst das Problem, den "Pfad" und den Ursprung einer Ausnahme herauszufinden?
VolkerK
1
Ich denke, Ihre Lösung befindet sich bereits in SO: stackoverflow.com/questions/77005/… Ich habe die hier beschriebene Lösung verwendet und sie funktioniert einwandfrei.
Neuro
2
Sie sollten in Betracht ziehen, das Betriebssystem über ein Tag anzugeben. Da Sie gdb erwähnen, würde ich annehmen, dass Sie nach einer Linux-Lösung suchen und nicht nach Windows.
Jschmier

Antworten:

71

Hier einige Informationen , die möglicherweise in Debuggen Problem von Nutzen sein

Wenn eine Ausnahme nicht erfasst wird, wird die spezielle Bibliotheksfunktion std::terminate()automatisch aufgerufen. Beenden ist eigentlich ein Zeiger auf eine Funktion und der Standardwert ist die Standard-C-Bibliotheksfunktion std::abort(). Wenn für eine nicht erfasste Ausnahme keine Bereinigungen auftreten , kann dies beim Debuggen dieses Problems hilfreich sein, da keine Destruktoren aufgerufen werden.
† Es ist implementierungsdefiniert, ob der Stapel vor dem std::terminate()Aufruf abgewickelt wird oder nicht .


Ein Aufruf von abort()ist häufig hilfreich, um einen Core-Dump zu generieren, der analysiert werden kann, um die Ursache der Ausnahme zu ermitteln. Stellen Sie sicher, dass Sie Core Dumps über ulimit -c unlimited(Linux) aktivieren .


Sie können Ihre eigene terminate()Funktion mit installieren std::set_terminate(). Sie sollten in der Lage sein, einen Haltepunkt für Ihre Beendigungsfunktion in gdb festzulegen. Möglicherweise können Sie aus Ihrer terminate()Funktion einen Stack-Backtrace generieren. Dieser Backtrace kann dabei helfen, den Ort der Ausnahme zu ermitteln.

Es gibt eine kurze Diskussion über nicht erfasste Ausnahmen in Bruce Eckels Denken in C ++, 2. Ausgabe , die ebenfalls hilfreich sein kann.


Da standardmäßig terminate()Aufrufe ausgeführt abort()werden (die standardmäßig ein SIGABRTSignal verursachen ), können Sie möglicherweise einen SIGABRTHandler festlegen und dann eine Stapelrückverfolgung aus dem Signalhandler heraus drucken . Diese Rückverfolgung kann bei der Identifizierung des Speicherorts der Ausnahme hilfreich sein.


Hinweis: Ich sage möglicherweise, weil C ++ die nicht-lokale Fehlerbehandlung durch die Verwendung von Sprachkonstrukten unterstützt, um die Fehlerbehandlung und den Berichtscode vom normalen Code zu trennen. Der Fangblock kann und befindet sich häufig in einer anderen Funktion / Methode als der Wurfpunkt. In den Kommentaren wurde ich auch darauf hingewiesen (danke Dan ), dass die Implementierung definiert ist, ob der Stapel vor dem terminate()Aufruf abgewickelt wird oder nicht .

Update: Ich habe ein Linux-Testprogramm namens aufgerufen, das eine Rückverfolgung in einem terminate()Funktionssatz über set_terminate()und eine andere in einem Signalhandler für generiert SIGABRT. Beide Rückverfolgungen zeigen den Ort der nicht behandelten Ausnahme korrekt an.

Update 2: Dank eines Blogposts über das Abfangen nicht erfasster Ausnahmen innerhalb von terminate habe ich einige neue Tricks gelernt. einschließlich des erneuten Auslösens der nicht erfassten Ausnahme innerhalb des Terminate-Handlers. Es ist wichtig zu beachten, dass die leere throwAnweisung im benutzerdefinierten Terminate-Handler mit GCC funktioniert und keine tragbare Lösung ist.

Code:

#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);
}

Ausgabe:

my_terminate hat eine unbehandelte Ausnahme abgefangen. what (): RUNTIME ERROR!
my_terminate backtrace hat 10 Frames zurückgegeben

[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]

Signal 6 (abgebrochen), Adresse ist 0x1239 von 0x42029331
krit_err_hdlr backtrace gab 13 Frames zurück

[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]

jschmier
quelle
1
Sehr interessant. Ich hatte immer den Verdacht, dass eine nicht behandelte Ausnahme den Stapel abwickeln würde, bis er die oberste Ebene erreicht ( main) und dann aufgerufen würde terminate(). Ihr Beispiel zeigt jedoch, dass überhaupt kein Abwickeln erfolgt, was sehr cool ist.
Dan
6
1) Die throw(int)Spezifikation ist nicht erforderlich. 2) Das uc->uc_mcontext.eipist wahrscheinlich sehr plattformabhängig (z. B. Verwendung ...ripauf einer 64-Bit-Plattform). 3) Kompilieren Sie mit, -rdynamicdamit Sie Backtrace-Symbole erhalten. 4) Laufen Sie ./a.out 2>&1 | c++filt, um hübsche Backtrace-Symbole zu erhalten.
Dan
2
"Für eine nicht erfasste Ausnahme werden keine Bereinigungen durchgeführt." - Eigentlich ist das implementierungsdefiniert. Siehe 15.3 / 9 und 15.5.1 / 2 in der C ++ - Spezifikation. "In der Situation, in der kein passender Handler gefunden wird, wird implementierungsdefiniert, ob der Stapel vor dem Aufruf von terminate () abgewickelt wird oder nicht." Trotzdem ist dies eine großartige Lösung, wenn Ihr Compiler dies unterstützt!
Dan
1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;arbeitete für mein ARM-Ziel
Stephen
1
Ein paar Anmerkungen: backtrace_symbols () führt ein Malloc aus. Daher möchten Sie möglicherweise beim Start einen Speicherblock vorab zuweisen und ihn dann in my_terminate () freigeben, bevor Sie backtrace_symbols () aufrufen, falls dies der Fall ist Behandlung einer std :: bad_alloc () - Ausnahme. Sie können auch <cxxabi.h> einschließen und dann __cxa_demangle () verwenden, um aus dem verstümmelten Teilstring, der zwischen '(' und '+' in den Zeichenfolgen der Ausgabemeldungen [] angezeigt wird, etwas Nützliches zu machen.
K Scott Piel
51

Wie Sie sagen, können wir in gdb 'catch throw' verwenden und für jede einzelne ausgelöste Ausnahme 'backtrace' aufrufen. Während dies normalerweise zu mühsam ist, um es manuell zu tun, ermöglicht gdb die Automatisierung des Prozesses. Auf diese Weise können Sie die Rückverfolgung aller Ausnahmen sehen, die ausgelöst werden, einschließlich der letzten nicht erfassten:

gdb>

set pagination off
catch throw
commands
backtrace
continue
end
run

Ohne weitere manuelle Eingriffe werden viele Rückverfolgungen generiert, einschließlich einer für die letzte nicht erfasste Ausnahme:

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.

Hier ist ein großartiger Blog-Beitrag, der dies zusammenfasst: http://741mhz.com/throw-stacktrace [auf archive.org]

TimJ
quelle
17

Sie können ein Makro wie folgt erstellen:

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

... und es gibt Ihnen den Ort, an dem die Ausnahme ausgelöst wird (zugegebenermaßen nicht die Stapelverfolgung). Sie müssen Ihre Ausnahmen von einer Basisklasse ableiten, die den obigen Konstruktor verwendet.

Erik Hermansen
quelle
18
-1 Sie nicht, throw new excation(...)aber throw exception(...)C ++ ist nicht Java,
Artyom
7
Okay, ich habe es behoben. Verzeihen Sie einem Programmierer, der vielleicht sowohl in Java als auch in C ++ funktioniert?
Erik Hermansen
Während ich das benutzt habe. Das Problem dabei ist, dass es nicht sagt, was die Ausnahme tatsächlich ausgelöst hat. Wenn Sie zum Beispiel 5 Stoi-Anrufe in einem Try-Block haben, wissen Sie nicht, welcher tatsächlich der Schuldige ist.
Banjocat
5

Sie haben keine Informationen darüber übergeben, welches Betriebssystem / welchen Compiler Sie verwenden.

In Visual Studio C ++ können Ausnahmen instrumentiert werden.

Siehe "Visual C ++ Exception-Handling Instrumentation" auf ddj.com

Mein Artikel "Postmortem Debugging" , ebenfalls auf ddj.com, enthält Code zur Verwendung der strukturierten Win32-Ausnahmebehandlung (von der Instrumentierung verwendet) für die Protokollierung usw.

ROTE WEICHE ADAIR
quelle
er sagte gdb, was Windows / Visual Studio so gut wie ausschließt.
Ben Voigt
2
Nun, er sagt, er hätte gerne etwas "kurz vor GDB", aber er bezieht sich nicht explizit auf ein Betriebssystem / einen Compiler. Das ist das Problem, dass Leute solche Sachen nicht deklarieren.
RED SOFT ADAIR
5

Sie können die wichtigsten engen Stellen in Ihrem Code markieren noexcept, um eine Ausnahme zu lokalisieren, und dann libunwind verwenden (einfach -lunwindzu den Linker-Parametern hinzufügen ) (getestet mit 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
}

}

Es gibt einen guten Artikel zu diesem Thema.

Tomilov Anatoliy
quelle
1

Ich habe Code, um dies in Windows / Visual Studio zu tun. Lassen Sie mich wissen, wenn Sie eine Gliederung wünschen. Ich weiß nicht, wie ich es für dwarf2-Code machen soll. Ein kurzer Blick auf Google schlägt vor, dass es in libgcc eine Funktion _Unwind_Backtrace gibt, die wahrscheinlich Teil dessen ist, was Sie brauchen.

Ben Voigt
quelle
Wahrscheinlich, weil "Lass es mich wissen, wenn du eine Gliederung willst" keine nützliche Antwort ist. Aber _Unwind_Backtrace ist; kompensieren.
Thomas
Aufgrund der Tatsache, dass das OP GDB erwähnte, vermutete ich, dass Windows nicht relevant war. Alex war natürlich frei, seine Frage zu bearbeiten, um Windows zu sagen.
Ben Voigt