Come faccio a trovare dove è stata generata un'eccezione in C ++?


92

Ho un programma che genera un'eccezione non rilevata da qualche parte. Tutto quello che ottengo è un rapporto di un'eccezione generata e nessuna informazione su dove è stata lanciata. Sembra illogico per un programma compilato per contenere simboli di debug non notificarmi dove nel mio codice è stata generata un'eccezione.

C'è un modo per dire da dove provengono le mie eccezioni se non impostando "catch throw" in gdb e chiamando un backtrace per ogni singola eccezione generata?



Cattura l'eccezione e guarda qual è il messaggio interno. Poiché è buona pratica che un'eccezione derivi da una delle eccezioni standard (std :: runtime_error) dovresti essere in grado di catturarla con catch (std :: exception const & e)
Martin York

1
E std :: exception / Std :: runtime_error risolve il problema di scoprire il "percorso" e l'origine di un'eccezione?
VolkerK

1
Poiché la tua domanda afferma gdb, penso che la tua soluzione sia già in SO: stackoverflow.com/questions/77005/… Ho usato la soluzione descritta qui e funziona perfettamente.
neuro

2
Dovresti considerare di specificare il sistema operativo tramite un tag. Dato che parli di gdb, presumo che tu stia cercando una soluzione Linux e non Windows.
jschmier

Risposte:


72

Ecco alcune informazioni che potrebbero essere utili per il debug del tuo problema

Se un'eccezione non viene rilevata, std::terminate()viene chiamata automaticamente la funzione di libreria speciale . Terminate è in realtà un puntatore a una funzione e il valore predefinito è la funzione della libreria C standard std::abort(). Se non si verificano pulizie per un'eccezione non rilevata , può effettivamente essere utile per il debug di questo problema poiché non viene chiamato alcun distruttore.
† È definito dall'implementazione se lo stack viene svolto o meno prima di std::terminate()essere chiamato.


Una chiamata a abort()è spesso utile per generare un core dump che può essere analizzato per determinare la causa dell'eccezione. Assicurati di abilitare i core dump tramite ulimit -c unlimited(Linux).


È possibile installare la propria terminate()funzione utilizzando std::set_terminate(). Dovresti essere in grado di impostare un punto di interruzione sulla funzione di terminazione in gdb. Si può essere in grado di generare un backtrace dello stack dal vostro terminate()funzione e questo backtrace può aiutare a identificare la posizione del l'eccezione.

C'è una breve discussione sulle eccezioni non gestite fanno in pensiero di Bruce Eckel in C ++, 2nd Ed che possono essere utili pure.


Dal momento che terminate()le chiamate abort()di default (che causerà un SIGABRTsegnale di default), si potrebbe essere in grado di impostare un SIGABRTgestore e poi stampare un backtrace dello stack all'interno del gestore di segnale . Questo backtrace può aiutare a identificare la posizione dell'eccezione.


Nota: dico maggio perché C ++ supporta la gestione degli errori non locali attraverso l'uso di costrutti di linguaggio per separare la gestione degli errori e il codice di reporting dal codice ordinario. Il blocco di cattura può essere, e spesso lo è, situato in una funzione / metodo diverso dal punto di lancio. Mi è stato anche fatto notare nei commenti (grazie Dan ) che è definito dall'implementazione indipendentemente dal fatto che lo stack venga svolto o meno prima di terminate()essere chiamato.

Aggiornamento: ho messo insieme un programma di test Linux chiamato che genera un backtrace in una terminate()funzione impostata tramite set_terminate()e un altro in un gestore di segnali per SIGABRT. Entrambi i backtrace mostrano correttamente la posizione dell'eccezione non gestita.

Aggiornamento 2: grazie a un post sul blog sulla cattura delle eccezioni non rilevate all'interno di terminate , ho imparato alcuni nuovi trucchi; incluso il rilancio dell'eccezione non rilevata all'interno del gestore di terminazione. È importante notare che l' throwistruzione vuota all'interno del gestore di terminazione personalizzato funziona con GCC e non è una soluzione portatile.

Codice:

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

Produzione:

my_terminate ha rilevato un'eccezione non gestita. what (): RUNTIME ERROR!
my_terminate backtrace ha restituito 10 frame

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

segnale 6 (interrotto), l'indirizzo è 0x1239 da 0x42029331
crit_err_hdlr backtrace ha restituito 13 frame

[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
Molto interessante. Ho sempre sospettato che un'eccezione non gestita avrebbe srotolato lo stack finché non fosse arrivato al livello più alto ( main) e poi avrebbe chiamato terminate(). Ma il tuo esempio mostra che non viene eseguito alcuno svolgimento, il che è molto interessante.
Dan

6
1) La throw(int)specifica non è necessaria. 2) uc->uc_mcontext.eipProbabilmente dipende molto dalla piattaforma (ad esempio, l'uso ...ripsu una piattaforma a 64 bit). 3) Compila con in -rdynamicmodo da ottenere simboli di backtrace. 4) Corri./a.out 2>&1 | c++filt per ottenere bei simboli di backtrace.
Dan

2
"Nessuna pulizia viene eseguita per un'eccezione non rilevata." - In realtà, questo è definito dall'implementazione. Vedere 15.3 / 9 e 15.5.1 / 2 nelle specifiche C ++. "Nella situazione in cui non viene trovato alcun gestore corrispondente, è definito dall'implementazione se lo stack viene rimosso o meno prima che venga chiamato terminate ()." Tuttavia, questa è un'ottima soluzione se il tuo compilatore lo supporta!
Dan

1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;ha funzionato per il mio obiettivo ARM
Stephen

1
Un paio di note: backtrace_symbols () fa un malloc ... quindi, potresti voler pre-allocare un blocco di memoria all'avvio, quindi deallocarlo in my_terminate () appena prima di chiamare backtrace_symbols () nel caso in cui ti trovi gestire un'eccezione std :: bad_alloc (). Inoltre, puoi includere <cxxabi.h> e quindi usare __cxa_demangle () per creare qualcosa di utile dalla sottostringa alterata visualizzata tra '(' e '+' nelle stringhe dei messaggi di output [].
K Scott Piel

51

Come dici tu, possiamo usare 'catch throw' in gdb e chiamare 'backtrace' per ogni singola eccezione generata. Anche se di solito è troppo noioso da fare manualmente, gdb consente l'automazione del processo. Ciò consente di vedere il backtrace di tutte le eccezioni lanciate, inclusa l'ultima non rilevata:

gdb>

set pagination off
catch throw
commands
backtrace
continue
end
run

Senza ulteriore intervento manuale, questo genera molti backtrace, incluso uno per l'ultima eccezione non rilevata:

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.

Ecco un ottimo post sul blog che lo conclude: http://741mhz.com/throw-stacktrace [su archive.org]


17

Puoi creare una macro come:

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

... e ti darà la posizione in cui viene lanciata l'eccezione (certamente non la traccia dello stack). È necessario che tu derivi le tue eccezioni da una classe base che accetta il costruttore di cui sopra.


18
-1 Non lo fai throw new excation(...)ma il throw exception(...)C ++ non è Java,
Artyom

7
Va bene, l'ho risolto. Perdonate forse un programmatore che funziona sia in Java che in C ++?
Erik Hermansen

Mentre l'ho usato. Il problema è che non dice cosa ha effettivamente generato l'eccezione. Se ad esempio hai 5 chiamate stoi in un blocco try non saprai quale sia effettivamente il colpevole.
Banjocat

5

Non hai trasmesso informazioni su quale sistema operativo / compilatore utilizzi.

In Visual Studio C ++ le eccezioni possono essere strumentate.

Vedere "Strumentazione per la gestione delle eccezioni di Visual C ++" su ddj.com

Il mio articolo "Postmortem Debugging" , anche su ddj.com include il codice per utilizzare la gestione strutturata delle eccezioni Win32 (usata dalla strumentazione) per la registrazione, ecc.


ha detto gdb, che praticamente esclude Windows / Visual Studio.
Ben Voigt

2
Beh, dice che vorrebbe qualcosa "a corto di gdb", ma non si riferisce esplicitamente a nessun sistema operativo / compilatore. Questo è il problema delle persone che non dichiarano cose del genere.
RED SOFT ADAIR

5

Puoi contrassegnare i principali punti stretti nel tuo codice come noexceptper individuare un'eccezione, quindi utilizzare libunwind (basta aggiungere -lunwindai parametri del linker) (testato con 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
}

}

C'è un buon articolo sull'argomento.


1

Ho il codice per farlo in Windows / Visual Studio, fammi sapere se vuoi uno schema. Non so come farlo per il codice dwarf2, però, un rapido google suggerisce che c'è una funzione _Unwind_Backtrace in libgcc che probabilmente fa parte di ciò di cui hai bisogno.


Probabilmente perché "fammi sapere se vuoi una bozza" non è una risposta utile. Ma _Unwind_Backtrace lo è; compensare.
Thomas

Sulla base del fatto che l'OP menzionava gdb, ho immaginato che Windows non fosse rilevante. Alex era, ovviamente, libero di modificare la sua domanda per dire Windows.
Ben Voigt

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.