Como descubro onde uma exceção foi lançada em C ++?

92

Eu tenho um programa que lança uma exceção não detectada em algum lugar. Tudo que recebo é um relatório de uma exceção sendo lançada, e nenhuma informação sobre onde ela foi lançada. Parece ilógico que um programa compilado para conter símbolos de depuração não me notifique sobre onde uma exceção foi gerada em meu código.

Existe alguma maneira de saber de onde estão vindo minhas exceções antes de definir 'catch throw' no gdb e chamar um backtrace para cada exceção lançada?

Alex
fonte
Capture a exceção e veja qual é a mensagem interna. Uma vez que é uma boa prática que uma exceção seja derivada de uma das exceções padrão (std :: runtime_error), você deve ser capaz de capturá-la com catch (std :: exception const & e)
Martin York
1
E std :: exception / Std :: runtime_error resolve o problema de descobrir o "caminho" e a origem de uma exceção?
VolkerK
1
Como sua pergunta indica gdb, acho que sua solução já está no SO: stackoverflow.com/questions/77005/… Usei a solução descrita aqui e ela funciona perfeitamente.
neuro
2
Você deve considerar a especificação do sistema operacional por meio de uma tag. Já que você mencionou o gdb, presumo que você esteja procurando uma solução Linux e não Windows.
jschmier

Respostas:

72

Aqui estão algumas informações que podem ser úteis para depurar seu problema

Se uma exceção não for detectada, a função de biblioteca especial std::terminate()é chamada automaticamente. Terminate é, na verdade, um ponteiro para uma função e o valor padrão é a função da biblioteca C padrão std::abort(). Se nenhuma limpeza ocorrer para uma exceção não detectada , pode ser útil na depuração desse problema, pois nenhum destruidor é chamado.
† É definido pela implementação se a pilha é ou não desfeita antes de std::terminate()ser chamada.


Uma chamada para abort()geralmente é útil para gerar um dump de memória que pode ser analisado para determinar a causa da exceção. Certifique-se de habilitar core dumps via ulimit -c unlimited(Linux).


Você pode instalar sua própria terminate()função usando std::set_terminate(). Você deve ser capaz de definir um ponto de interrupção em sua função encerrar no gdb. Você pode ser capaz de gerar um backtrace da pilha a partir de sua terminate()função e esse backtrace pode ajudar a identificar a localização da exceção.

Há uma breve discussão sobre exceções não detectadas em Thinking in C ++, 2ª edição de Bruce Eckel que também pode ser útil.


Como as terminate()chamadas são abort()por padrão (o que irá causar um SIGABRTsinal por padrão), você pode definir um SIGABRTmanipulador e, em seguida, imprimir um rastreamento de pilha de dentro do manipulador de sinal . Este backtrace pode ajudar a identificar a localização da exceção.


Observação: posso dizer que sim porque C ++ oferece suporte ao tratamento de erros não locais por meio do uso de construções de linguagem para separar o tratamento de erros e o código de relatório do código comum. O bloco de captura pode estar, e freqüentemente está, localizado em uma função / método diferente do ponto de lançamento. Também foi apontado para mim nos comentários (obrigado Dan ) que é definido pela implementação quer a pilha seja desfeita ou não antes de terminate()ser chamada.

Atualização: Eu juntei um programa de teste Linux chamado que gera um backtrace em um terminate()conjunto de funções via set_terminate()e outro em um manipulador de sinal para SIGABRT. Ambos os backtraces mostram corretamente a localização da exceção não tratada.

Atualização 2: graças a uma postagem do blog sobre Captura de exceções não capturadas no terminate , aprendi alguns truques novos; incluindo o relançamento da exceção não capturada dentro do manipulador de terminação. É importante observar que a throwinstrução vazia dentro do manipulador de terminação personalizado funciona com o GCC e não é uma solução 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);
}

Resultado:

my_terminate detectou uma exceção espontânea. what (): RUNTIME ERROR!
my_terminate backtrace retornou 10 frames

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

sinal 6 (abortado), o endereço é 0x1239 de 0x42029331
crit_err_hdlr backtrace retornou 13 frames

[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
fonte
1
Muito interessante. Sempre suspeitei que uma exceção não tratada iria desenrolar a pilha até chegar ao nível superior ( main) e então chamar terminate(). Mas seu exemplo mostra que nenhum desenrolamento é feito, o que é muito legal.
Dan,
6
1) A throw(int)especificação é desnecessária. 2) uc->uc_mcontext.eipProvavelmente depende muito da plataforma (por exemplo, use ...ripem uma plataforma de 64 bits). 3) Compile com -rdynamicpara obter símbolos de backtrace. 4) Corra ./a.out 2>&1 | c++filtpara obter símbolos de backtrace bonitos.
Dan,
2
"Nenhuma limpeza ocorre para uma exceção não detectada." - Na verdade, isso é definido pela implementação. Consulte 15.3 / 9 e 15.5.1 / 2 nas especificações C ++. "Na situação em que nenhum manipulador correspondente é encontrado, é definido pela implementação se a pilha é ou não desfeita antes que terminate () seja chamado." Ainda assim, esta é uma ótima solução se o seu compilador oferecer suporte!
Dan
1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;trabalhou para meu alvo ARM
stephen
1
Algumas notas: backtrace_symbols () faz um malloc ... então, você pode querer pré-alocar um bloco de memória na inicialização, então desalocá-lo em my_terminate () antes de chamar backtrace_symbols () no caso de você estar manipulando uma exceção std :: bad_alloc (). Além disso, você pode incluir <cxxabi.h> e usar __cxa_demangle () para fazer algo útil com a substring mutilada exibida entre '(' e '+' nas mensagens de saída [] strings.
K Scott Piel
51

Como você disse, podemos usar 'catch throw' no gdb e chamar 'backtrace' para cada exceção lançada. Embora isso geralmente seja muito tedioso para fazer manualmente, o gdb permite a automação do processo. Isso permite ver o backtrace de todas as exceções que são lançadas, incluindo a última não capturada:

gdb>

set pagination off
catch throw
commands
backtrace
continue
end
run

Sem outra intervenção manual, isso gera muitos rastros de retorno, incluindo um para a última exceção não 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.

Aqui está uma ótima postagem do blog resumindo isso: http://741mhz.com/throw-stacktrace [no archive.org]

TimJ
fonte
17

Você pode criar uma macro como:

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

... e ele lhe dará o local onde a exceção é lançada (reconhecidamente não o rastreamento de pilha). É necessário que você derive suas exceções de alguma classe base que usa o construtor acima.

Erik Hermansen
fonte
18
-1 Você não, throw new excation(...)mas throw exception(...)C ++ não é Java,
Artyom
7
Ok, eu consertei. Perdoe um programador que trabalhe tanto em Java quanto em C ++, talvez?
Erik Hermansen,
Enquanto eu uso isso. O problema é que não informa o que realmente gerou a exceção. Se, por exemplo, você tiver 5 chamadas de stoi em um bloco try, não saberá qual delas é o culpado.
Banjocat
5

Você não passou informações sobre qual SO / Compilador você usa.

No Visual Studio C ++, as exceções podem ser instrumentadas.

Consulte "Instrumentação para tratamento de exceções do Visual C ++" em ddj.com

Meu artigo "Postmortem Debugging" , também no ddj.com inclui código para usar o tratamento de exceções estruturadas do Win32 (usado pela instrumentação) para registro, etc.

RED SOFT ADAIR
fonte
ele disse gdb, que praticamente exclui o Windows / Visual Studio.
Ben Voigt de
2
Bem, ele diz que gostaria de algo "menor que gdb", mas não se refere explicitamente a nenhum SO / Compilador. Esse é o problema das pessoas não declararem essas coisas.
RED SOFT ADAIR
5

Você pode marcar os principais locais restritos em seu código noexceptpara localizar uma exceção e, em seguida, usar o libunwind (basta adicionar -lunwindaos parâmetros do vinculador) (testado com 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
}

}

Existe um bom artigo sobre o assunto.

Tomilov Anatoliy
fonte
1

Eu tenho um código para fazer isso no Windows / Visual Studio, deixe-me saber se você quiser um esboço. Não sei como fazer isso para o código dwarf2, porém, um google rápido sugere que há uma função _Unwind_Backtrace em libgcc que provavelmente é parte do que você precisa.

Ben Voigt
fonte
Provavelmente porque "deixe-me saber se você quiser um esboço" não é uma resposta útil. Mas _Unwind_Backtrace é; compensando.
Thomas,
Com base no fato de que o OP mencionava gdb, imaginei que o Windows não era relevante. Alex estava, é claro, livre para editar sua pergunta e dizer Windows.
Ben Voigt de