Rastreamento de pilha de exibição C ++ em exceção

204

Desejo ter uma maneira de relatar o rastreamento de pilha ao usuário se uma exceção for lançada. Qual é a melhor maneira de fazer isso? É preciso uma quantidade enorme de código extra?

Para responder perguntas:

Eu gostaria que fosse portátil, se possível. Quero que as informações sejam exibidas, para que o usuário possa copiar o rastreamento da pilha e enviá-lo por e-mail para mim se ocorrer um erro.

rlbond
fonte

Respostas:

76

Depende de qual plataforma.

No GCC, é bastante trivial, veja este post para mais detalhes.

No MSVC, você pode usar a biblioteca StackWalker que lida com todas as chamadas de API subjacentes necessárias para o Windows.

Você precisará descobrir a melhor maneira de integrar essa funcionalidade ao seu aplicativo, mas a quantidade de código que você precisa escrever deve ser mínima.

Andrew Grant
fonte
71
a postagem que você vincula aponta principalmente para gerar um rastreio a partir de um segfault, mas o solicitante menciona especificamente exceções, que são uma fera bem diferente.
Shep
8
Concordo com o @Shep - essa resposta não ajuda muito a obter um rastreamento de pilha do código de lançamento no GCC. Veja minha resposta para uma possível solução.
Thomas Tempelmann
1
Esta resposta é enganosa. O link aponta para uma resposta específica para Linuxnão gcc.
fjardon
Você pode substituir o mecanismo de lançamento libstdc++(usado pelo GCC e potencialmente por Clang), conforme explicado nesta resposta .
ingomueller.net 5/09/19
59

A resposta de Andrew Grant não ajuda a obter um rastreamento de pilha da função throwing , pelo menos não com o GCC, porque uma instrução throw não salva o rastreamento de pilha atual por conta própria e o manipulador de captura não terá acesso ao rastreamento de pilha em esse ponto mais.

A única maneira - usando o GCC - de resolver isso é gerar um rastreamento de pilha no ponto da instrução throw e salvá-lo com o objeto de exceção.

Esse método exige, é claro, que todo código que lança uma exceção use essa classe Exception específica.

Atualização 11 de julho de 2017 : Para obter um código útil, dê uma olhada na resposta de cahit beyaz, que aponta para http://stacktrace.sourceforge.net - ainda não o usei, mas parece promissor.

Thomas Tempelmann
fonte
1
Infelizmente, o link está morto. Você poderia fornecer outro?
warran
2
E archive.org também não sabe disso. Droga. Bem, o procedimento deve ser claro: lance um objeto de classe personalizada que registra o rastreamento da pilha no momento do lance.
Thomas Tempelmann
1
Na página inicial do StackTrace, entendo throw stack_runtime_error. Estou correto ao deduzir que essa lib funciona apenas para exceções derivadas dessa classe e não para std::exceptionou exceções de bibliotecas de terceiros?
Thomas
3
Infelizmente, a resposta é "Não, você não pode obter um rastreamento de pilha de uma exceção C ++", a única opção é lançar sua própria classe que gera um rastreamento de pilha quando é construído. Se você estiver usando coisas como, digamos, qualquer parte da biblioteca C ++ std ::, não terá sorte. Desculpe, é uma merda ser você.
Code Abominator
43

Se você estiver usando o Boost 1.65 ou superior, poderá usar o boost :: stacktrace :

#include <boost/stacktrace.hpp>

// ... somewhere inside the bar(int) function that is called recursively:
std::cout << boost::stacktrace::stacktrace();
vaso
fonte
5
Os documentos de impulso explicam não apenas a captura de um rastreamento de pilha, mas como fazer isso para exceções e declarações. Coisas boas.
Moodboom 14/01/19
1
Esse stacktrace () imprime o arquivo de origem e os números de linha, conforme fornecido no guia GettingStarted?
Gimhani 29/01/19
11

Gostaria de adicionar uma opção de biblioteca padrão (ou seja, plataforma cruzada) como gerar backtraces de exceção, que se tornou disponível no C ++ 11 :

Use std::nested_exceptionestd::throw_with_nested

Isso não dará a você uma pilha para relaxar, mas, na minha opinião, a próxima melhor coisa. É descrito no StackOverflow aqui e aqui , como você pode obter um retorno das suas exceções dentro do seu código sem a necessidade de um depurador ou log complicado, simplesmente escrevendo um manipulador de exceções adequado que irá repetir exceções aninhadas.

Como você pode fazer isso com qualquer classe de exceção derivada, você pode adicionar muitas informações a esse backtrace! Você também pode dar uma olhada no meu MWE no GitHub , onde um backtrace seria algo como isto:

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
fonte
Provavelmente, isso é muito melhor, se você estiver disposto a fazer um trabalho extra, do que o rastreamento usual de pilha burra.
Clearer
4

O AFAIK libunwind é bastante portátil e até agora não encontrei nada mais fácil de usar.

Nico Brailovsky
fonte
A libunwind 1.1 não se baseia no os x.
Xaxxon # 20/16
4

Eu recomendo http://stacktrace.sourceforge.net/ project. Ele suporta Windows, Mac OS e também Linux

cahit beyaz
fonte
4
Na sua página inicial, entendo throw stack_runtime_error. Estou correto ao deduzir que essa lib funciona apenas para exceções derivadas dessa classe e não para std::exceptionou exceções de bibliotecas de terceiros?
Thomas
4

Se você estiver usando C ++ e não quiser / não puder usar o Boost, poderá imprimir o backtrace com nomes desmembrados usando o seguinte código [link para o site original] .

Observe que esta solução é específica para Linux. Ele usa as funções libc do GNU, backtrace () / backtrace_symbols () (de execinfo.h) para obter os backtraces e, em seguida, usa __cxa_demangle () (de cxxabi.h) para desmantelar os nomes dos símbolos 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
fonte
3

No Windows, confira BugTrap . Não está mais no link original, mas ainda está disponível no CodeProject.

jww
fonte
3

Tenho um problema semelhante e, embora goste de portabilidade, preciso apenas de suporte ao gcc. No gcc, o execinfo.h e as chamadas de backtrace estão disponíveis. Para desmontar os nomes das funções, o Sr. Bingmann possui um bom pedaço de código. Para despejar um backtrace em uma exceção, eu crio uma exceção que imprime o backtrace no construtor. Se eu esperava que isso funcionasse com uma exceção lançada em uma biblioteca, pode ser necessário reconstruir / vincular para que a exceção de retorno seja usada.

/******************************************
#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 e executar isso com o gcc 4.8.4 gera um backtrace com nomes de funções C ++ não-manipuladas:

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

Como a pilha já está desenrolada ao entrar no bloco catch, a solução no meu caso foi não capturar certas exceções que levam a um SIGABRT. No manipulador de sinal do SIGABRT, então fork () e execl () gdb (em compilações de depuração) ou stackpads do Google breakpads (em compilações de versão). Também tento usar apenas funções seguras do manipulador de sinal.

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: Para fazê-lo funcionar para o breakpad, eu também tive que adicionar isso:

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

Fonte: Como obter um rastreamento de pilha para C ++ usando o gcc com informações de número de linha? e É possível anexar o gdb a um processo travado (também conhecido como depuração "just-in-time")

Bl00dh0und
fonte
2

Poppy pode coletar não apenas o rastreamento da pilha, mas também valores de parâmetros, variáveis ​​locais, etc. - tudo o que levou à falha.

Orlin Georgiev
fonte
2

O código a seguir interrompe a execução logo após uma exceção ser lançada. Você precisa definir um windows_exception_handler junto com um manipulador de terminação. Eu testei isso em 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 o seguinte código para a função windows_exception_handler: http://www.codedisqus.com/0ziVPgVPUk/exception-handling-and-stacktrace-under-windows-mingwgcc.html

Marcos Fuentes
fonte
1

Cpp-tool ex_diag - peso fácil, multiplataforma, uso mínimo de recursos, simples e flexível no rastreamento.

Boris
fonte
Eu verifiquei este projeto em 24.12.2017, fonte e download, ambos não acessíveis.
Zhaorufei
1
Acabei de verificar o code.google.com/archive/p/exception-diagnostic/source/default/… e a fonte está disponível para download. Você pode tentar mais uma vez?
Boris