Trace de pile d'affichage C ++ en cas d'exception

204

Je veux avoir un moyen de signaler la trace de la pile à l'utilisateur si une exception est levée. Quelle est la meilleure façon de procéder? Faut-il d'énormes quantités de code supplémentaire?

Pour répondre aux questions:

J'aimerais que ce soit portable si possible. Je souhaite que des informations apparaissent, afin que l'utilisateur puisse copier la trace de la pile et m'envoyer un e-mail en cas d'erreur.

rlbond
la source

Réponses:

76

Cela dépend de quelle plateforme.

Sur GCC c'est assez trivial, voir ce post pour plus de détails.

Sur MSVC, vous pouvez utiliser la bibliothèque StackWalker qui gère tous les appels d'API sous-jacents nécessaires pour Windows.

Vous devrez trouver la meilleure façon d'intégrer cette fonctionnalité dans votre application, mais la quantité de code que vous devez écrire devrait être minimale.

Andrew Grant
la source
71
le message auquel vous liez indique principalement la génération d'une trace à partir d'une erreur de segmentation, mais le demandeur mentionne spécifiquement les exceptions, qui sont une bête très différente.
Shep
8
Je suis d'accord avec @Shep - cette réponse n'aide pas vraiment à obtenir une trace de pile du code de lancement sur GCC. Voir ma réponse pour une solution possible.
Thomas Tempelmann
1
Cette réponse est trompeuse. Le lien pointe vers une réponse spécifique à Linuxnot gcc.
fjardon
Vous pouvez remplacer le mécanisme de lancement de libstdc++(utilisé par GCC et potentiellement Clang) comme expliqué dans cette réponse .
ingomueller.net
59

La réponse d'Andrew Grant n'aide pas à obtenir une trace de pile de la fonction de lancement , du moins pas avec GCC, car une instruction throw n'enregistre pas la trace de pile actuelle seule, et le gestionnaire de capture n'aura pas accès à la trace de pile à ce point plus.

La seule façon - en utilisant GCC - de résoudre ce problème est de vous assurer de générer une trace de pile au point de l'instruction throw et de l'enregistrer avec l'objet d'exception.

Cette méthode nécessite, bien sûr, que chaque code qui lève une exception utilise cette classe d'exception particulière.

Mise à jour du 11 juillet 2017 : pour un code utile, jetez un œil à la réponse de cahit beyaz, qui pointe vers http://stacktrace.sourceforge.net - je ne l'ai pas encore utilisé mais cela semble prometteur.

Thomas Tempelmann
la source
1
Malheureusement, le lien est mort. Pourriez-vous en fournir un autre?
warran
2
Et archive.org ne le sait pas non plus. Zut. Eh bien, la procédure doit être claire: lancer un objet de classe personnalisé qui enregistre la trace de la pile au moment du lancer.
Thomas Tempelmann
1
Sur la page d'accueil de StackTrace, je vois throw stack_runtime_error. Ai-je raison de déduire que cette bibliothèque ne fonctionne que pour les exceptions dérivées de cette classe, et non pour les std::exceptionexceptions de bibliothèques tierces?
Thomas
3
Malheureusement, la réponse est "Non, vous ne pouvez pas obtenir une trace de pile à partir d'une exception C ++", la seule option est de lancer votre propre classe qui génère une trace de pile lors de sa construction. Si vous êtes bloqué en utilisant des choses comme, disons, n'importe quelle partie de la bibliothèque std :: C ++, vous n'avez pas de chance. Désolé, ça craint d'être toi.
Code Abominator
43

Si vous utilisez Boost 1.65 ou supérieur, vous pouvez utiliser boost :: stacktrace :

#include <boost/stacktrace.hpp>

// ... somewhere inside the bar(int) function that is called recursively:
std::cout << boost::stacktrace::stacktrace();
vasek
la source
5
Les documents boost expliquent non seulement la capture d'une trace de pile, mais comment le faire pour les exceptions et les assertions. Super truc.
moodboom
1
Est-ce que stacktrace () imprime le fichier source et les numéros de ligne comme indiqué dans le guide GettingStarted?
Gimhani
14

Unix: backtrace

Mac: trace

Windows: CaptureBackTrace

bobobobo
la source
2
Le lien de trace Mac est mort.
Codie CodeMonkey
Lien actif pour la trace Mac (pour iOS) . Semble fonctionner correctement avec OSX 10.14.6 Mojave.
jt bullitt
11

Je voudrais ajouter une option de bibliothèque standard (c'est-à-dire multiplateforme) comment générer des backtraces d'exception, qui est devenue disponible avec C ++ 11 :

Utiliser std::nested_exceptionetstd::throw_with_nested

Cela ne vous permettra pas de vous détendre, mais à mon avis, la meilleure chose à faire. Il est décrit sur StackOverflow ici et ici , comment vous pouvez obtenir une trace arrière sur vos exceptions dans votre code sans avoir besoin d'un débogueur ou d'une journalisation encombrante, en écrivant simplement un gestionnaire d'exceptions approprié qui renverra les exceptions imbriquées.

Puisque vous pouvez le faire avec n'importe quelle classe d'exception dérivée, vous pouvez ajouter beaucoup d'informations à une telle trace! Vous pouvez également jeter un oeil à mon MWE sur GitHub , où une trace ressemblerait à quelque chose comme ceci:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"
GPMueller
la source
C'est probablement beaucoup mieux, si vous êtes prêt à faire le travail supplémentaire, que la trace de pile stupide habituelle.
Plus clair
4

AFAIK libunwind est assez portable et jusqu'à présent je n'ai rien trouvé de plus facile à utiliser.

Nico Brailovsky
la source
libunwind 1.1 ne s'appuie pas sur os x.
xaxxon
4

Je recommande le projet http://stacktrace.sourceforge.net/ . Il prend en charge Windows, Mac OS et aussi Linux

cahit beyaz
la source
4
Sur sa page d'accueil, je vois throw stack_runtime_error. Ai-je raison de déduire que cette bibliothèque ne fonctionne que pour les exceptions dérivées de cette classe, et non pour les std::exceptionexceptions de bibliothèques tierces?
Thomas
4

Si vous utilisez C ++ et que vous ne voulez pas / ne pouvez pas utiliser Boost, vous pouvez imprimer une trace arrière avec des noms démangés en utilisant le code suivant [lien vers le site d'origine] .

Notez que cette solution est spécifique à Linux. Il utilise les fonctions libc de GNU backtrace () / backtrace_symbols () (from execinfo.h) pour obtenir les backtraces, puis utilise __cxa_demangle () (from cxxabi.h) pour démêler les noms des symboles de backtrace.

// stacktrace.h (c) 2008, Timo Bingmann from http://idlebox.net/
// published under the WTFPL v2.0

#ifndef _STACKTRACE_H_
#define _STACKTRACE_H_

#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>
#include <cxxabi.h>

/** Print a demangled stack backtrace of the caller function to FILE* out. */
static inline void print_stacktrace(FILE *out = stderr, unsigned int max_frames = 63)
{
    fprintf(out, "stack trace:\n");

    // storage array for stack trace address data
    void* addrlist[max_frames+1];

    // retrieve current stack addresses
    int addrlen = backtrace(addrlist, sizeof(addrlist) / sizeof(void*));

    if (addrlen == 0) {
    fprintf(out, "  <empty, possibly corrupt>\n");
    return;
    }

    // resolve addresses into strings containing "filename(function+address)",
    // this array must be free()-ed
    char** symbollist = backtrace_symbols(addrlist, addrlen);

    // allocate string which will be filled with the demangled function name
    size_t funcnamesize = 256;
    char* funcname = (char*)malloc(funcnamesize);

    // iterate over the returned symbol lines. skip the first, it is the
    // address of this function.
    for (int i = 1; i < addrlen; i++)
    {
    char *begin_name = 0, *begin_offset = 0, *end_offset = 0;

    // find parentheses and +address offset surrounding the mangled name:
    // ./module(function+0x15c) [0x8048a6d]
    for (char *p = symbollist[i]; *p; ++p)
    {
        if (*p == '(')
        begin_name = p;
        else if (*p == '+')
        begin_offset = p;
        else if (*p == ')' && begin_offset) {
        end_offset = p;
        break;
        }
    }

    if (begin_name && begin_offset && end_offset
        && begin_name < begin_offset)
    {
        *begin_name++ = '\0';
        *begin_offset++ = '\0';
        *end_offset = '\0';

        // mangled name is now in [begin_name, begin_offset) and caller
        // offset in [begin_offset, end_offset). now apply
        // __cxa_demangle():

        int status;
        char* ret = abi::__cxa_demangle(begin_name,
                        funcname, &funcnamesize, &status);
        if (status == 0) {
        funcname = ret; // use possibly realloc()-ed string
        fprintf(out, "  %s : %s+%s\n",
            symbollist[i], funcname, begin_offset);
        }
        else {
        // demangling failed. Output function name as a C function with
        // no arguments.
        fprintf(out, "  %s : %s()+%s\n",
            symbollist[i], begin_name, begin_offset);
        }
    }
    else
    {
        // couldn't parse the line? print the whole line.
        fprintf(out, "  %s\n", symbollist[i]);
    }
    }

    free(funcname);
    free(symbollist);
}

#endif // _STACKTRACE_H_

HTH!

sundeep singh
la source
3

Sous Windows, consultez BugTrap . Ce n'est plus au lien d'origine, mais il est toujours disponible sur CodeProject.

jww
la source
3

J'ai un problème similaire, et bien que j'aime la portabilité, je n'ai besoin que du support gcc. Dans gcc, execinfo.h et les appels de trace sont disponibles. Pour démêler les noms de fonction, M. Bingmann a un joli morceau de code. Pour vider une trace arrière sur une exception, je crée une exception qui imprime la trace arrière dans le constructeur. Si je m'attendais à ce que cela fonctionne avec une exception levée dans une bibliothèque, cela pourrait nécessiter une reconstruction / liaison afin que l'exception de retour en arrière soit utilisée.

/******************************************
#Makefile with flags for printing backtrace with function names
# compile with symbols for backtrace
CXXFLAGS=-g
# add symbols to dynamic symbol table for backtrace
LDFLAGS=-rdynamic
turducken: turducken.cc
******************************************/

#include <cstdio>
#include <stdexcept>
#include <execinfo.h>
#include "stacktrace.h" /* https://panthema.net/2008/0901-stacktrace-demangled/ */

// simple exception that prints backtrace when constructed
class btoverflow_error: public std::overflow_error
{
    public:
    btoverflow_error( const std::string& arg ) :
        std::overflow_error( arg )
    {
        print_stacktrace();
    };
};


void chicken(void)
{
    throw btoverflow_error( "too big" );
}

void duck(void)
{
    chicken();
}

void turkey(void)
{
    duck();
}

int main( int argc, char *argv[])
{
    try
    {
        turkey();
    }
    catch( btoverflow_error e)
    {
        printf( "caught exception: %s\n", e.what() );
    }
}

La compilation et l'exécution de ceci avec gcc 4.8.4 donne une trace avec des noms de fonction C ++ bien démêlés:

stack trace:
 ./turducken : btoverflow_error::btoverflow_error(std::string const&)+0x43
 ./turducken : chicken()+0x48
 ./turducken : duck()+0x9
 ./turducken : turkey()+0x9
 ./turducken : main()+0x15
 /lib/x86_64-linux-gnu/libc.so.6 : __libc_start_main()+0xf5
 ./turducken() [0x401629]
Thomas
la source
3

Étant donné que la pile est déjà déroulée lors de l'entrée dans le bloc catch, la solution dans mon cas était de ne pas intercepter certaines exceptions qui conduisent ensuite à un SIGABRT. Dans le gestionnaire de signaux pour SIGABRT, puis fork () et execl () soit gdb (dans les versions de débogage) ou Google breakpads stackwalk (dans les versions de version). J'essaie également d'utiliser uniquement les fonctions de sécurité du gestionnaire de signaux.

GDB:

static const char BACKTRACE_START[] = "<2>--- backtrace of entire stack ---\n";
static const char BACKTRACE_STOP[] = "<2>--- backtrace finished ---\n";

static char *ltrim(char *s)
{
    while (' ' == *s) {
        s++;
    }
    return s;
}

void Backtracer::print()
{
    int child_pid = ::fork();
    if (child_pid == 0) {
        // redirect stdout to stderr
        ::dup2(2, 1);

        // create buffer for parent pid (2+16+1 spaces to allow up to a 64 bit hex parent pid)
        char pid_buf[32];
        const char* stem = "                   ";
        const char* s = stem;
        char* d = &pid_buf[0];
        while (static_cast<bool>(*s))
        {
            *d++ = *s++;
        }
        *d-- = '\0';
        char* hexppid = d;

        // write parent pid to buffer and prefix with 0x
        int ppid = getppid();
        while (ppid != 0) {
            *hexppid = ((ppid & 0xF) + '0');
            if(*hexppid > '9') {
                *hexppid += 'a' - '0' - 10;
            }
            --hexppid;
            ppid >>= 4;
        }
        *hexppid-- = 'x';
        *hexppid = '0';

        // invoke GDB
        char name_buf[512];
        name_buf[::readlink("/proc/self/exe", &name_buf[0], 511)] = 0;
        ssize_t r = ::write(STDERR_FILENO, &BACKTRACE_START[0], sizeof(BACKTRACE_START));
        (void)r;
        ::execl("/usr/bin/gdb",
                "/usr/bin/gdb", "--batch", "-n", "-ex", "thread apply all bt full", "-ex", "quit",
                &name_buf[0], ltrim(&pid_buf[0]), nullptr);
        ::exit(1); // if GDB failed to start
    } else if (child_pid == -1) {
        ::exit(1); // if forking failed
    } else {
        // make it work for non root users
        if (0 != getuid()) {
            ::prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);
        }
        ::waitpid(child_pid, nullptr, 0);
        ssize_t r = ::write(STDERR_FILENO, &BACKTRACE_STOP[0], sizeof(BACKTRACE_STOP));
        (void)r;
    }
}

minidump_stackwalk:

static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded)
{
    int child_pid = ::fork();
    if (child_pid == 0) {
        ::dup2(open("/dev/null", O_WRONLY), 2); // ignore verbose output on stderr
        ssize_t r = ::write(STDOUT_FILENO, &MINIDUMP_STACKWALK_START[0], sizeof(MINIDUMP_STACKWALK_START));
        (void)r;
        ::execl("/usr/bin/minidump_stackwalk", "/usr/bin/minidump_stackwalk", descriptor.path(), "/usr/share/breakpad-syms", nullptr);
        ::exit(1); // if minidump_stackwalk failed to start
    } else if (child_pid == -1) {
        ::exit(1); // if forking failed
    } else {
        ::waitpid(child_pid, nullptr, 0);
        ssize_t r = ::write(STDOUT_FILENO, &MINIDUMP_STACKWALK_STOP[0], sizeof(MINIDUMP_STACKWALK_STOP));
        (void)r;
    }
    ::remove(descriptor.path()); // this is not signal safe anymore but should still work
    return succeeded;
}

Edit: Pour le faire fonctionner pour le breakpad, j'ai également dû ajouter ceci:

std::set_terminate([]()
{
    ssize_t r = ::write(STDERR_FILENO, EXCEPTION, sizeof(EXCEPTION));
    (void)r;
    google_breakpad::ExceptionHandler::WriteMinidump(std::string("/tmp"), dumpCallback, NULL);
    exit(1); // avoid creating a second dump by not calling std::abort
});

Source: Comment obtenir une trace de pile pour C ++ en utilisant gcc avec des informations sur le numéro de ligne? et est-il possible d'attacher gdb à un processus en panne (aka débogage "juste à temps")

Bl00dh0und
la source
2

Poppy peut rassembler non seulement la trace de la pile, mais aussi les valeurs des paramètres, les variables locales, etc. - tout ce qui mène au crash.

Orlin Georgiev
la source
2

Le code suivant arrête l'exécution juste après la levée d'une exception. Vous devez définir un windows_exception_handler avec un gestionnaire de terminaison. J'ai testé cela en MinGW 32bits.

void beforeCrash(void);

static const bool SET_TERMINATE = std::set_terminate(beforeCrash);

void beforeCrash() {
    __asm("int3");
}

int main(int argc, char *argv[])
{
SetUnhandledExceptionFilter(windows_exception_handler);
...
}

Vérifiez le code suivant pour la fonction windows_exception_handler: http://www.codedisqus.com/0ziVPgVPUk/exception-handling-and-stacktrace-under-windows-mingwgcc.html

Marcos Fuentes
la source