C ++ display stack trace en excepción

204

Quiero tener una manera de informar el seguimiento de la pila al usuario si se produce una excepción. ¿Cuál es la mejor manera de hacer esto? ¿Se necesita una gran cantidad de código extra?

Para responder preguntas:

Me gustaría que sea portátil si es posible. Quiero que aparezca información emergente, para que el usuario pueda copiar el seguimiento de la pila y enviarme un correo electrónico si aparece un error.

rlbond
fuente

Respuestas:

76

Depende de qué plataforma.

En GCC es bastante trivial, vea esta publicación para más detalles.

En MSVC, puede usar la biblioteca StackWalker que maneja todas las llamadas API subyacentes necesarias para Windows.

Tendrá que encontrar la mejor manera de integrar esta funcionalidad en su aplicación, pero la cantidad de código que necesita escribir debe ser mínima.

Andrew Grant
fuente
71
la publicación a la que se vincula en su mayoría apunta a generar un rastro de una falla predeterminada, pero el autor de la pregunta menciona específicamente excepciones, que son una bestia bastante diferente.
Shep
8
Estoy de acuerdo con @Shep: esta respuesta realmente no ayuda a obtener un seguimiento de la pila del código de lanzamiento en GCC. Vea mi respuesta para una posible solución.
Thomas Tempelmann
1
Esta respuesta es engañosa. El enlace apunta a una respuesta específica para Linuxno gcc.
fjardon
Puede anular el mecanismo de lanzamiento de libstdc++(utilizado por GCC y potencialmente Clang) como se explica en esta respuesta .
ingomueller.net
59

La respuesta de Andrew Grant no ayuda a obtener un seguimiento de la pila de la función de lanzamiento , al menos no con GCC, porque una instrucción throw no guarda el seguimiento de la pila actual por sí solo, y el controlador catch no tendrá acceso al seguimiento de la pila en ese punto más.

La única forma, usando GCC, de resolver esto es asegurándose de generar un seguimiento de la pila en el punto de la instrucción de lanzamiento, y guardarlo con el objeto de excepción.

Este método requiere, por supuesto, que cada código que arroje una excepción use esa clase de excepción en particular.

Actualización 11 de julio de 2017 : para obtener un código útil, eche un vistazo a la respuesta de cahit beyaz, que apunta a http://stacktrace.sourceforge.net . Todavía no lo he usado, pero parece prometedor.

Thomas Tempelmann
fuente
1
Lamentablemente, el enlace está muerto. ¿Podrías proporcionar alguna otra?
Warran
2
Y archive.org tampoco lo sabe. Maldición. Bueno, el procedimiento debe ser claro: arroje un objeto de clase personalizada que registre el seguimiento de la pila en el momento del lanzamiento.
Thomas Tempelmann
1
En la página de inicio de StackTrace, ya veo throw stack_runtime_error. ¿Estoy en lo cierto al deducir que esta biblioteca solo funciona para excepciones derivadas de esa clase, y no para std::exceptionexcepciones de bibliotecas de terceros?
Thomas
3
Lamentablemente, la respuesta es "No, no puede obtener un seguimiento de la pila de una excepción de C ++", la única opción es lanzar su propia clase que genera un seguimiento de la pila cuando se construye. Si está atascado usando cosas como, por ejemplo, cualquier parte de la biblioteca C ++ std ::, no tiene suerte. Lo siento, apesta ser tú.
Code Abominator
43

Si está utilizando Boost 1.65 o superior, puede usar boost :: stacktrace :

#include <boost/stacktrace.hpp>

// ... somewhere inside the bar(int) function that is called recursively:
std::cout << boost::stacktrace::stacktrace();
vasek
fuente
55
Los documentos de impulso explican no solo la captura de un seguimiento de la pila, sino cómo hacerlo para excepciones y afirmaciones. Buena cosa.
moodboom
1
¿Este stacktrace () imprime el archivo fuente y los números de línea como se indica en la guía GettingStarted?
Gimhani
11

Me gustaría agregar una opción de biblioteca estándar (es decir, multiplataforma) sobre cómo generar rastreos de excepción, que está disponible con C ++ 11 :

Uso std::nested_exceptionystd::throw_with_nested

Esto no te dará un montón de descanso, pero en mi opinión lo mejor. Se describe en StackOverflow aquí y aquí , cómo puede obtener un seguimiento de sus excepciones dentro de su código sin necesidad de un depurador o un registro engorroso, simplemente escribiendo un controlador de excepciones adecuado que arroje excepciones anidadas.

Como puede hacer esto con cualquier clase de excepción derivada, ¡puede agregar mucha información a dicha traza inversa! También puede echar un vistazo a mi MWE en GitHub , donde una traza inversa se vería así:

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
fuente
Probablemente esto sea mucho mejor, si está dispuesto a hacer el trabajo extra, que la traza de pila tonta habitual.
Más claro
4

AFAIK libunwind es bastante portátil y hasta ahora no he encontrado nada más fácil de usar.

Nico Brailovsky
fuente
libunwind 1.1 no se basa en os x.
xaxxon
4

Recomiendo http://stacktrace.sourceforge.net/ project. Es compatible con Windows, Mac OS y también Linux

cahit beyaz
fuente
44
En su página de inicio, ya veo throw stack_runtime_error. ¿Estoy en lo cierto al deducir que esta biblioteca solo funciona para excepciones derivadas de esa clase, y no para std::exceptionexcepciones de bibliotecas de terceros?
Thomas
4

Si está utilizando C ++ y no quiere / no puede usar Boost, puede imprimir la traza inversa con los nombres solicitados utilizando el siguiente código [enlace al sitio original] .

Tenga en cuenta que esta solución es específica de Linux. Utiliza las funciones libc de GNU backtrace () / backtrace_symbols () (de execinfo.h) para obtener los retrocesos y luego usa __cxa_demangle () (de cxxabi.h) para solicitar los nombres de los símbolos de retroceso.

// 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
fuente
3

En Windows, echa un vistazo a BugTrap . Ya no está en el enlace original, pero todavía está disponible en CodeProject.

jww
fuente
3

Tengo un problema similar, y aunque me gusta la portabilidad, solo necesito soporte de gcc. En gcc, execinfo.h y los backtrace llamadas están disponibles. Para exigir los nombres de las funciones, el Sr. Bingmann tiene un buen código. Para volcar un rastreo en una excepción, creo una excepción que imprime el rastreo en el constructor. Si esperaba que esto funcionara con una excepción lanzada en una biblioteca, podría requerir la reconstrucción / vinculación para que se use la excepción de rastreo.

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

Compilar y ejecutar esto con gcc 4.8.4 produce una traza inversa con nombres de funciones de C ++ muy bien desarmados:

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
fuente
3

Como la pila ya está desenrollada al ingresar al bloque catch, la solución en mi caso fue no capturar ciertas excepciones que luego conducen a un SIGABRT. En el controlador de señal para SIGABRT, luego bifurco () y execl () gdb (en compilaciones de depuración) o stackpacks de Google breakpads (en compilaciones de lanzamiento). También trato de usar solo las funciones seguras del controlador de señal.

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

Editar: para que funcione para el breakpad también tuve que agregar esto:

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

Fuente: ¿Cómo obtener un seguimiento de pila para C ++ usando gcc con información de número de línea? y ¿Es posible adjuntar gdb a un proceso bloqueado (también conocido como depuración "justo a tiempo")

Bl00dh0und
fuente
2

Poppy puede recopilar no solo el seguimiento de la pila, sino también los valores de los parámetros, las variables locales, etc., todo lo que conduce al bloqueo.

Orlin Georgiev
fuente
2

El siguiente código detiene la ejecución justo después de que se lanza una excepción. Debe establecer un controlador_excepción_ windows junto con un controlador de terminación. Probé esto 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);
...
}

Verifique el siguiente código para la función windows_exception_handler: http://www.codedisqus.com/0ziVPgVPUk/exception-handling-and-stacktrace-under-windows-mingwgcc.html

Marcos Fuentes
fuente