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.