¿Cómo encuentro dónde se lanzó una excepción en C ++?

92

Tengo un programa que arroja una excepción no detectada en alguna parte. Todo lo que recibo es un informe de una excepción lanzada y ninguna información sobre dónde se lanzó. Parece ilógico que un programa compilado contenga símbolos de depuración que no me notifiquen en qué parte de mi código se generó una excepción.

¿Hay alguna forma de saber de dónde provienen mis excepciones a menos que establezca 'catch throw' en gdb y llame a un backtrace para cada excepción lanzada?

Alex
fuente
Detecte la excepción y vea cuál es el mensaje interno. Dado que es una buena práctica que una excepción se derive de una de las excepciones estándar (std :: runtime_error), debe poder detectarla con catch (std :: exception const & e)
Martin York
1
¿Y std :: exception / Std :: runtime_error resuelve el problema de encontrar la "ruta" y el origen de una excepción?
VolkerK
1
Como su pregunta indica gdb, creo que su solución ya está en SO: stackoverflow.com/questions/77005/… He usado la solución descrita aquí y funciona perfectamente.
neuro
2
Debería considerar especificar el sistema operativo mediante una etiqueta. Dado que menciona gdb, supongo que está buscando una solución de Linux y no de Windows.
jschmier

Respuestas:

72

Aquí hay información que puede ser útil para depurar su problema

Si no se detecta una excepción, std::terminate()se llama automáticamente a la función de biblioteca especial . Terminar es en realidad un puntero a una función y el valor predeterminado es la función de la biblioteca C estándar std::abort(). Si no se realizan limpiezas para una excepción no detectada , en realidad puede ser útil para depurar este problema, ya que no se llaman destructores.
† Está definido por la implementación si la pila se desenrolla o no antes de que std::terminate()se llame.


Una llamada a abort()suele ser útil para generar un volcado de memoria que se puede analizar para determinar la causa de la excepción. Asegúrese de habilitar los volcados de núcleo a través de ulimit -c unlimited(Linux).


Puede instalar su propia terminate()función usando std::set_terminate(). Debería poder establecer un punto de interrupción en su función de terminación en gdb. Es posible que pueda generar un seguimiento de pila a partir de su terminate()función y este seguimiento puede ayudar a identificar la ubicación de la excepción.

Hay una breve discusión sobre excepciones no detectadas en Thinking in C ++, 2nd Ed de Bruce Eckel que también puede ser útil.


Dado que las terminate()llamadas abort()de forma predeterminada (lo que provocará una SIGABRTseñal de forma predeterminada), es posible que pueda configurar un SIGABRTcontrolador y luego imprimir un seguimiento de la pila desde dentro del controlador de señal . Este seguimiento puede ayudar a identificar la ubicación de la excepción.


Nota: Digo que puede porque C ++ admite el manejo de errores no locales mediante el uso de construcciones de lenguaje para separar el manejo de errores y el código de informes del código ordinario. El bloque de captura puede estar, y a menudo está, ubicado en una función / método diferente al punto de lanzamiento. También se me ha señalado en los comentarios (gracias Dan ) que está definido por la implementación si la pila se desenrolla o no antes de terminate()llamar.

Actualización: reuní un programa de prueba de Linux llamado que genera un backtrace en un terminate()conjunto de funciones via set_terminate()y otro en un controlador de señal para SIGABRT. Ambos backtraces muestran correctamente la ubicación de la excepción no controlada.

Actualización 2: Gracias a una publicación de blog sobre cómo detectar excepciones no detectadas dentro de terminar , aprendí algunos trucos nuevos; incluido el relanzamiento de la excepción no detectada dentro del controlador de terminación. Es importante tener en cuenta que la throwdeclaración vacía dentro del controlador de terminación personalizado funciona con GCC y no es una solución portátil.

Código:

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

Salida:

my_terminate detectó una excepción sin mano. what (): RUNTIME ERROR!
my_terminate backtrace devolvió 10 fotogramas

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

señal 6 (cancelada), la dirección es 0x1239 de 0x42029331
crit_err_hdlr backtrace devolvió 13 cuadros

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

jschmier
fuente
1
Muy interesante. Siempre sospeché que una excepción no manejada desenrollaría la pila hasta que llegara al nivel superior ( main) y luego llamaría terminate(). Pero su ejemplo muestra que no se realiza ningún desenrollado, lo cual es muy bueno.
Dan
6
1) La throw(int)especificación es innecesaria. 2) uc->uc_mcontext.eipProbablemente dependa mucho de la plataforma (p. Ej., Se usa ...ripen una plataforma de 64 bits). 3) Compile con -rdynamicpara obtener símbolos de retroceso. 4) Corre ./a.out 2>&1 | c++filtpara obtener bonitos símbolos de retroceso.
Dan
2
"No se realizan limpiezas para una excepción no detectada". - En realidad, eso está definido por la implementación. Consulte 15.3 / 9 y 15.5.1 / 2 en la especificación de C ++. "En la situación en la que no se encuentra un controlador coincidente, la implementación se define si la pila se desenrolla o no antes de que se llame a terminate ()". Aún así, esta es una gran solución si su compilador la admite.
Dan
1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;trabajó para mi objetivo de ARM
Stephen
1
Un par de notas: backtrace_symbols () hace un malloc ... entonces, es posible que desee preasignar un bloque de memoria al inicio, luego desasignarlo en my_terminate () justo antes de llamar a backtrace_symbols () en caso de que esté manejando una excepción std :: bad_alloc (). Además, puede incluir <cxxabi.h> y luego usar __cxa_demangle () para hacer algo útil de la subcadena destrozada que se muestra entre '(' y '+' en las cadenas de mensajes de salida [].
K Scott Piel
51

Como dices, podemos usar 'catch throw' en gdb y llamar a 'backtrace' para cada excepción lanzada. Si bien eso suele ser demasiado tedioso para hacerlo manualmente, gdb permite la automatización del proceso. Eso permite ver el seguimiento de todas las excepciones que se lanzan, incluida la última no detectada:

gdb>

set pagination off
catch throw
commands
backtrace
continue
end
run

Sin más intervención manual, esto genera muchos rastreos, incluido uno para la última excepción no detectada:

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.

Aquí hay una excelente publicación de blog que concluye esto: http://741mhz.com/throw-stacktrace [en archive.org]

TimJ
fuente
17

Puede crear una macro como:

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

... y le dará la ubicación donde se lanza la excepción (es cierto que no el seguimiento de la pila). Es necesario que derive sus excepciones de alguna clase base que tome el constructor anterior.

Erik Hermansen
fuente
18
-1 No, throw new excation(...)pero throw exception(...)C ++ no es Java,
Artyom
7
Está bien, lo arreglé. ¿Perdonar a un programador que trabaja tanto en Java como en C ++, tal vez?
Erik Hermansen
Mientras he usado esto. El problema con esto es que no dice qué arrojó realmente la excepción. Si, por ejemplo, tiene 5 llamadas stoi en un bloque de prueba, no sabrá cuál es realmente la culpable.
Banjocat
5

No pasó información sobre el sistema operativo / compilador que utiliza.

En Visual Studio C ++ se pueden instrumentar excepciones.

Consulte "Instrumentación de manejo de excepciones de Visual C ++" en ddj.com

Mi artículo "Depuración post mortem" , también en ddj.com, incluye código para usar el manejo de excepciones estructurado de Win32 (usado por la instrumentación) para el registro, etc.

ADAIR SUAVE ROJO
fuente
dijo gdb, que prácticamente descarta Windows / Visual Studio.
Ben Voigt
2
Bueno, él dice que le gustaría algo "menos de gdb", pero no se refiere explícitamente a ningún sistema operativo / compilador. Ese es el problema de la gente que no declara tales cosas.
RED SOFT ADAIR
5

Puede marcar los lugares estrechos principales en su código noexceptpara ubicar una excepción, luego use libunwind (solo agregue -lunwinda los parámetros del vinculador) (probado 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
}

}

Hay un buen artículo sobre el tema.

Tomilov Anatoliy
fuente
1

Tengo un código para hacer esto en Windows / Visual Studio, avíseme si quiere un esquema. Sin embargo, no sé cómo hacerlo para el código dwarf2, una búsqueda rápida en Google sugiere que hay una función _Unwind_Backtrace en libgcc que probablemente sea parte de lo que necesita.

Ben Voigt
fuente
Probablemente porque "avíseme si quiere un bosquejo" no es una respuesta útil. Pero _Unwind_Backtrace es; compensar.
Thomas
Sobre la base de que el OP mencionó gdb, supuse que Windows no era relevante. Alex era, por supuesto, libre de editar su pregunta para decir Windows.
Ben Voigt