Śledzenie stosu wyświetlania C ++ w wyjątku

204

Chcę mieć sposób na zgłoszenie śladu stosu użytkownikowi, jeśli zostanie zgłoszony wyjątek. Jak najlepiej to zrobić? Czy to wymaga dużej ilości dodatkowego kodu?

Aby odpowiedzieć na pytania:

Chciałbym, żeby to było przenośne, jeśli to możliwe. Chcę, aby pojawiły się informacje, aby użytkownik mógł skopiować ślad stosu i wysłać mi wiadomość e-mail, jeśli pojawi się błąd.

rlbond
źródło

Odpowiedzi:

76

To zależy od platformy.

Na GCC jest to dość trywialne, zobacz ten post, aby uzyskać więcej informacji.

W MSVC możesz użyć biblioteki StackWalker, która obsługuje wszystkie wywołania API bazowe potrzebne dla systemu Windows.

Musisz znaleźć najlepszy sposób na zintegrowanie tej funkcji z aplikacją, ale ilość kodu, którą musisz napisać, powinna być minimalna.

Andrew Grant
źródło
71
post, do którego linkujesz, głównie wskazuje na wygenerowanie śladu po segfault, ale pytający konkretnie wymienia wyjątki, które są zupełnie inną bestią.
Shep
8
Zgadzam się z @Shep - ta odpowiedź tak naprawdę nie pomaga w uzyskaniu śladu stosu kodu wyrzucającego na GCC. Zobacz moją odpowiedź na możliwe rozwiązanie.
Thomas Tempelmann,
1
Ta odpowiedź jest myląca. Link wskazuje na odpowiedź specyficzną na „ Linuxnie” gcc.
fjardon
Możesz zastąpić mechanizm rzucania libstdc++(używany przez GCC i potencjalnie Clanga), jak wyjaśniono w tej odpowiedzi .
ingomueller.net,
59

Odpowiedź Andrew Granta nie pomaga w uzyskaniu śladu stosu funkcji rzucania , przynajmniej nie w przypadku GCC, ponieważ instrukcja throw nie zapisuje bieżącego śladu stosu samodzielnie, a moduł obsługi przechwytywania nie będzie miał dostępu do śladu stosu na ten punkt już.

Jedynym sposobem - za pomocą GCC - aby rozwiązać ten problem, jest wygenerowanie śledzenia stosu w punkcie instrukcji rzucania i zapisanie go z obiektem wyjątku.

Ta metoda wymaga oczywiście, aby każdy kod zgłaszający wyjątek używał tej konkretnej klasy wyjątków.

Aktualizacja 11 lipca 2017 r . : Aby uzyskać pomocny kod, spójrz na odpowiedź cahit beyaz, która wskazuje na http://stacktrace.sourceforge.net - jeszcze go nie użyłem, ale wygląda obiecująco.

Thomas Tempelmann
źródło
1
Niestety link nie działa. Czy możesz podać jakieś inne?
warran
2
Archive.org też tego nie wie. Cholera. Cóż, procedura powinna być jasna: rzuć obiekt klasy niestandardowej, który rejestruje ślad stosu w momencie rzutu.
Thomas Tempelmann
1
Widzę na stronie głównej StackTrace throw stack_runtime_error. Czy mam rację, wydedukowując, że ta biblioteka działa tylko dla wyjątków pochodzących z tej klasy, a nie dla std::exceptionwyjątków z bibliotek stron trzecich?
Thomas
3
Niestety odpowiedź brzmi: „Nie, nie można uzyskać śladu stosu z wyjątku C ++”, jedyną opcją jest wrzucenie własnej klasy, która generuje ślad stosu podczas jego budowy. Jeśli utkniesz w używaniu np. Dowolnej części biblioteki std :: C ++, nie masz szczęścia. Przepraszam, do bani jesteś ty.
Kod Abominator
43

Jeśli używasz Boost 1.65 lub wyższej, możesz użyć boost :: stacktrace :

#include <boost/stacktrace.hpp>

// ... somewhere inside the bar(int) function that is called recursively:
std::cout << boost::stacktrace::stacktrace();
vasek
źródło
5
Dokumenty pomocnicze wyjaśniają nie tylko przechwytywanie śladu stosu, ale także jak to zrobić dla wyjątków i stwierdzeń. Świetne rzeczy.
moodboom
1
Czy ta funkcja stacktrace () drukuje plik źródłowy i numery linii podane w przewodniku Pierwsze kroki?
Gimhani
14

Unix: ślad

Mac: ślad

Windows: CaptureBackTrace

Bobobobo
źródło
2
Łącze śledzenia Maca nie działa.
Codie CodeMonkey
Aktywny link do śledzenia Macintosha (dla iOS) . Wygląda na to, że działa poprawnie z OSX 10.14.6 Mojave.
jt bullitt
11

Chciałbym dodać standardową opcję biblioteki (tj. Wieloplatformową), jak generować wyjątki śledzenia, które stały się dostępne w C ++ 11 :

Użyj std::nested_exceptionistd::throw_with_nested

To nie zapewni ci odprężenia stosu, ale moim zdaniem kolejna najlepsza rzecz. Jest to opisane na StackOverflow tutaj i tutaj , w jaki sposób można uzyskać ślad na swoim wyjątkami wewnątrz kodu bez konieczności uciążliwego debugger lub logowania, po prostu pisząc odpowiedni program obsługi wyjątków, które będą rethrow zagnieżdżone wyjątki.

Ponieważ możesz to zrobić z dowolną pochodną klasą wyjątku, możesz dodać wiele informacji do takiego śladu wstecznego! Możesz także rzucić okiem na moją MWE na GitHub , gdzie ślad może wyglądać mniej więcej tak:

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
źródło
Jest to prawdopodobnie o wiele lepsze, jeśli chcesz wykonać dodatkową pracę, niż zwykłe śledzenie stosu.
Jaśniejsze
4

AFAIK libunwind jest dość przenośny i do tej pory nie znalazłem nic łatwiejszego w użyciu.

Nico Brailovsky
źródło
libunwind 1.1 nie działa na systemie OS X.
xaxxon
4

Polecam projekt http://stacktrace.sourceforge.net/ . Obsługuje Windows, Mac OS, a także Linux

Cahit Beyaz
źródło
4
Widzę na stronie głównej throw stack_runtime_error. Czy mam rację, wydedukowując, że ta biblioteka działa tylko dla wyjątków pochodzących z tej klasy, a nie dla std::exceptionwyjątków z bibliotek stron trzecich?
Thomas
4

Jeśli używasz C ++ i nie chcesz / nie możesz użyć Boost, możesz wydrukować ślad ze zdemodowanymi nazwami, używając następującego kodu [link do oryginalnej strony] .

Uwaga: to rozwiązanie jest specyficzne dla systemu Linux. Używa funkcji libc GNU backtrace () / backtrace_symbols () (z execinfo.h), aby uzyskać ślady wsteczne, a następnie używa __cxa_demangle () (z cxxabi.h) do rozplątywania nazw symboli śledzenia.

// 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
źródło
3

W systemie Windows sprawdź BugTrap . Nie jest już pod oryginalnym linkiem, ale nadal jest dostępny w CodeProject.

jww
źródło
3

Mam podobny problem i chociaż lubię przenośność, potrzebuję tylko obsługi gcc. W gcc dostępne są wywołania execinfo.h i backtrace . Aby rozplątać nazwy funkcji, pan Bingmann ma niezły kawałek kodu. Aby zrzucić ślad wyjątku, tworzę wyjątek, który drukuje ślad w konstruktorze. Jeśli spodziewałem się, że zadziała to z wyjątkiem zgłoszonym w bibliotece, może to wymagać przebudowania / połączenia, aby można było zastosować wyjątek śledzenia wstecznego.

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

Kompilacja i uruchomienie tego z gcc 4.8.4 daje ślad wsteczny z ładnie niezmienionymi nazwami funkcji C ++:

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]
Tomasz
źródło
3

Ponieważ stos jest już rozwijany po wejściu do bloku catch, w moim przypadku rozwiązaniem było nie wychwycenie pewnych wyjątków które następnie prowadziły do ​​SIGABRT. W module obsługi sygnałów dla SIGABRT I następnie fork () i execl () albo gdb (w kompilacjach debugowania), albo Google breakpads stosu stosów (w kompilacjach wersji). Staram się również używać tylko bezpiecznych funkcji obsługi sygnałów.

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

Edycja: Aby działało na breakpad, musiałem również dodać:

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

Źródło: Jak uzyskać ślad stosu dla C ++ przy użyciu gcc z informacjami o numerze linii? oraz Czy można dołączyć gdb do zawieszonego procesu (czyli debugowania „just-in-time”)

Bl00dh0und
źródło
2

Poppy może gromadzić nie tylko ślad stosu, ale także wartości parametrów, zmienne lokalne itp. - wszystko, co prowadzi do awarii.

Orlin Georgiew
źródło
2

Poniższy kod zatrzymuje wykonywanie zaraz po zgłoszeniu wyjątku. Musisz ustawić moduł obsługi wyjątków systemu Windows wraz z procedurą obsługi zakończenia. Przetestowałem to w MinGW 32 bity.

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

Sprawdź następujący kod funkcji Windows_exception_handler: http://www.codedisqus.com/0ziVPgVPUk/exception-handling-and-stacktrace-under-windows-mingwgcc.html

Marcos Fuentes
źródło