Comment trouver où une exception a été levée en C ++?


92

J'ai un programme qui lance une exception non interceptée quelque part. Tout ce que j'obtiens, c'est un rapport sur une exception lancée, et aucune information sur l'endroit où elle a été lancée. Il semble illogique pour un programme compilé de contenir des symboles de débogage de ne pas me notifier où dans mon code une exception a été générée.

Existe-t-il un moyen de savoir d'où viennent mes exceptions sans définir 'catch throw' dans gdb et appeler une trace pour chaque exception levée?



Attrapez l'exception et voyez ce qu'est le message interne. Comme c'est une bonne pratique pour une exception d'être dérivée de l'une des exceptions standard (std :: runtime_error), vous devriez pouvoir l'attraper avec catch (std :: exception const & e)
Martin York

1
Et std :: exception / Std :: runtime_error résout le problème de trouver le "chemin" et l'origine d'une exception?
VolkerK

1
Comme votre question énonce gdb, je pense que votre solution est déjà dans SO: stackoverflow.com/questions/77005/ ... J'ai utilisé la solution décrite ici et cela fonctionne parfaitement.
neuro

2
Vous devriez envisager de spécifier le système d'exploitation via une balise. Puisque vous mentionnez gdb, je suppose que vous recherchez une solution Linux et non Windows.
jschmier

Réponses:


72

Voici quelques informations qui peuvent être utiles pour déboguer votre problème

Si une exception n'est pas interceptée, la fonction de bibliothèque spéciale std::terminate()est automatiquement appelée. Terminate est en fait un pointeur vers une fonction et la valeur par défaut est la fonction de bibliothèque C standard std::abort(). Si aucun nettoyage n'a lieu pour une exception non interceptée , cela peut en fait être utile pour déboguer ce problème car aucun destructeur n'est appelé.
† Il est défini par l'implémentation, que la pile soit déroulée ou non avant d' std::terminate()être appelée.


Un appel à abort()est souvent utile pour générer un vidage de mémoire qui peut être analysé pour déterminer la cause de l'exception. Assurez-vous d'activer les vidages de mémoire via ulimit -c unlimited(Linux).


Vous pouvez installer votre propre terminate()fonction en utilisant std::set_terminate(). Vous devriez pouvoir définir un point d'arrêt sur votre fonction de terminaison dans gdb. Vous pourrez peut- être générer une trace arrière de pile à partir de votre terminate()fonction et cette trace arrière peut aider à identifier l'emplacement de l'exception.

Il y a une brève discussion sur les exceptions non détectées dans la pensée de Bruce Eckel en C ++, 2e édition qui peut également être utile.


Depuis les terminate()appels abort()par défaut (ce qui provoquera un SIGABRTsignal par défaut), vous pourrez peut- être définir un SIGABRTgestionnaire, puis imprimer une trace de pile à partir du gestionnaire de signaux . Cette trace arrière peut aider à identifier l'emplacement de l'exception.


Remarque: je dis peut parce que C ++ prend en charge la gestion des erreurs non locales via l'utilisation de constructions de langage pour séparer la gestion des erreurs et le code de rapport du code ordinaire. Le bloc catch peut être, et est souvent, situé dans une fonction / méthode différente de celle du point de lancement. On m'a également fait remarquer dans les commentaires (merci Dan ) qu'il est défini par l'implémentation que la pile soit déroulée ou non avant d' terminate()être appelée.

Mise à jour: J'ai lancé un programme de test Linux appelé qui génère une trace dans une terminate()fonction définie via set_terminate()et une autre dans un gestionnaire de signaux pour SIGABRT. Les deux backtraces affichent correctement l'emplacement de l'exception non gérée.

Mise à jour 2: Grâce à un article de blog sur la capture d'exceptions non interceptées dans terminate , j'ai appris quelques nouvelles astuces; y compris la relance de l'exception non interceptée dans le gestionnaire de terminaison. Il est important de noter que l' throwinstruction vide dans le gestionnaire de terminaison personnalisé fonctionne avec GCC et n'est pas une solution portable.

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

Production:

my_terminate a intercepté une exception non gérée. quoi (): ERREUR DE RUNTIME!
my_terminate backtrace a renvoyé 10 images

[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 (abandonné), l'adresse est 0x1239 à partir de 0x42029331
crit_err_hdlr backtrace a renvoyé 13 images

[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
Très intéressant. J'ai toujours suspecté qu'une exception non gérée déroulerait la pile jusqu'à ce qu'elle atteigne le niveau supérieur ( main), puis elle appellerait terminate(). Mais votre exemple montre qu'aucun déroulement n'est fait du tout, ce qui est très cool.
Dan

6
1) La throw(int)spécification est inutile. 2) Le uc->uc_mcontext.eipest probablement très dépendant de la plate-forme (par exemple, utilisation ...ripsur une plate-forme 64 bits). 3) Compilez avec -rdynamicpour obtenir des symboles de suivi. 4) Courez ./a.out 2>&1 | c++filtpour obtenir de jolis symboles de traces.
Dan

2
"Aucun nettoyage n'a lieu pour une exception non interceptée." - En fait, c'est défini par l'implémentation. Voir 15.3 / 9 et et 15.5.1 / 2 dans la spécification C ++. "Dans le cas où aucun gestionnaire correspondant n'est trouvé, l'implémentation définit si la pile est déroulée ou non avant l'appel de terminate ()." Pourtant, c'est une excellente solution si votre compilateur le prend en charge!
Dan

1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;travaillé pour ma cible ARM
stephen

1
Quelques notes: backtrace_symbols () fait un malloc ... donc, vous voudrez peut-être pré-allouer un bloc de mémoire au démarrage, puis le désallouer dans my_terminate () juste avant d'appeler backtrace_symbols () dans le cas où vous seriez gérer une exception std :: bad_alloc (). De plus, vous pouvez inclure <cxxabi.h> puis utiliser __cxa_demangle () pour créer quelque chose d'utile à partir de la sous-chaîne mutilée affichée entre '(' et '+' dans les chaînes de messages de sortie [].
K Scott Piel

51

Comme vous le dites, nous pouvons utiliser 'catch throw' dans gdb et appeler 'backtrace' pour chaque exception levée. Bien que cela soit généralement trop fastidieux à faire manuellement, gdb permet l'automatisation du processus. Cela permet de voir la trace de toutes les exceptions levées, y compris la dernière non interceptée:

gdb>

set pagination off
catch throw
commands
backtrace
continue
end
run

Sans autre intervention manuelle, cela génère de nombreuses backtraces, dont une pour la dernière exception non interceptée:

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.

Voici un excellent article de blog pour conclure ceci: http://741mhz.com/throw-stacktrace [sur archive.org]


17

Vous pouvez créer une macro comme:

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

... et il vous donnera l'emplacement où l'exception est lancée (certes pas la trace de pile). Il est nécessaire que vous dériviez vos exceptions à partir d'une classe de base qui prend le constructeur ci-dessus.


18
-1 Vous ne le faites pas throw new excation(...)mais throw exception(...)C ++ n'est pas Java,
Artyom

7
D'accord, je l'ai réparé. Pardonner à un programmeur qui fonctionne à la fois en Java et en C ++
Erik Hermansen

Bien que je l'ai utilisé. Le problème avec cela, c'est qu'il ne dit pas ce qui a réellement provoqué l'exception. Si, par exemple, vous avez 5 appels stoi dans un bloc try, vous ne saurez pas lequel est réellement le coupable.
Banjocat

5

Vous n'avez pas transmis d'informations sur le système d'exploitation / compilateur que vous utilisez.

Dans Visual Studio C ++, les exceptions peuvent être instrumentées.

Voir «Instrumentation de gestion des exceptions Visual C ++» sur ddj.com

Mon article "Postmortem Debugging" , également sur ddj.com, inclut du code pour utiliser la gestion des exceptions structurée Win32 (utilisée par l'instrumentation) pour la journalisation, etc.


il a dit gdb, ce qui exclut pratiquement Windows / Visual Studio.
Ben Voigt

2
Eh bien, il dit qu'il aimerait quelque chose de "court de gdb", mais il ne fait explicitement référence à aucun système d'exploitation / compilateur. C'est le problème des gens qui ne déclarent pas de telles choses.
RED SOFT ADAIR

5

Vous pouvez marquer les principaux endroits serrés dans votre code noexceptpour localiser une exception, puis utiliser libunwind (ajouter simplement -lunwindaux paramètres de l'éditeur de liens) (testé avec 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
}

}

Il y a un bon article sur la question.


1

J'ai du code pour le faire dans Windows / Visual Studio, faites-moi savoir si vous voulez un aperçu. Je ne sais pas comment le faire pour le code dwarf2 cependant, un rapide google suggère qu'il existe une fonction _Unwind_Backtrace dans libgcc qui fait probablement partie de ce dont vous avez besoin.


Probablement parce que "faites-moi savoir si vous voulez un aperçu" n'est pas une réponse utile. Mais _Unwind_Backtrace est; compenser.
Thomas

Sur la base du fait que l'OP mentionnait gdb, j'ai deviné que Windows n'était pas pertinent. Alex était, bien sûr, libre de modifier sa question pour dire Windows.
Ben Voigt

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.