C ++ menampilkan jejak stack terkecuali

204

Saya ingin memiliki cara untuk melaporkan jejak stack kepada pengguna jika ada pengecualian. Apa cara terbaik untuk melakukan ini? Apakah butuh kode ekstra dalam jumlah besar?

Untuk menjawab pertanyaan:

Saya ingin portabel jika mungkin. Saya ingin informasi muncul, sehingga pengguna dapat menyalin jejak tumpukan dan mengirim email kepada saya jika ada kesalahan.

rbbond
sumber

Jawaban:

76

Itu tergantung platform mana.

Di GCC itu sangat sepele, lihat posting ini untuk lebih jelasnya.

Pada MSVC maka Anda dapat menggunakan pustaka StackWalker yang menangani semua panggilan API mendasar yang diperlukan untuk Windows.

Anda harus mencari cara terbaik untuk mengintegrasikan fungsi ini ke dalam aplikasi Anda, tetapi jumlah kode yang Anda butuhkan harus minimal.

Andrew Grant
sumber
71
posting yang Anda tautkan sebagian besar mengarah ke menghasilkan jejak dari segfault, tetapi penanya secara khusus menyebutkan pengecualian, yang merupakan binatang yang sangat berbeda.
Shep
8
Saya setuju dengan @Shep - jawaban ini tidak benar-benar membantu dengan mendapatkan jejak stack dari kode melempar pada GCC. Lihat jawaban saya untuk solusi yang mungkin.
Thomas Tempelmann
1
Jawaban ini menyesatkan. Tautan menunjuk ke jawaban yang spesifik untuk Linuxtidak gcc.
fjardon
Anda dapat mengganti mekanisme melempar libstdc++(digunakan oleh GCC dan berpotensi Dentang) seperti yang dijelaskan dalam jawaban ini .
ingomueller.net
59

Jawaban Andrew Grant tidak membantu mendapatkan jejak stack dari fungsi throwing , setidaknya tidak dengan GCC, karena pernyataan throw tidak menyimpan jejak stack saat ini dengan sendirinya, dan catch handler tidak akan memiliki akses ke jejak stack di titik itu lagi.

Satu-satunya cara - menggunakan GCC - untuk menyelesaikan ini adalah dengan memastikan untuk menghasilkan jejak stack pada titik instruksi melempar, dan menyimpannya dengan objek pengecualian.

Metode ini tentu saja mengharuskan setiap kode yang melempar pengecualian menggunakan kelas Exception tertentu.

Pembaruan 11 Juli 2017 : Untuk beberapa kode bermanfaat, lihat jawaban cahit beyaz, yang menunjuk ke http://stacktrace.sourceforge.net - Saya belum pernah menggunakannya tetapi terlihat menjanjikan.

Thomas Tempelmann
sumber
1
Sayangnya tautannya sudah mati. Bisakah Anda memberikan beberapa lainnya?
warran
2
Dan archive.org juga tidak mengetahuinya. Sial. Nah, prosedurnya harus jelas: melempar objek kelas kustom yang merekam jejak stack pada saat melempar.
Thomas Tempelmann
1
Di halaman beranda StackTrace, saya mengerti throw stack_runtime_error. Apakah saya benar dalam menyimpulkan bahwa lib ini hanya berfungsi untuk pengecualian yang berasal dari kelas itu, dan bukan untuk std::exceptionatau pengecualian dari perpustakaan pihak ketiga?
Thomas
3
Jadi sayangnya jawabannya adalah "Tidak, Anda tidak bisa mendapatkan jejak stack dari pengecualian C ++", satu-satunya pilihan adalah membuang kelas Anda sendiri yang menghasilkan jejak stack ketika dibangun. Jika Anda terjebak menggunakan hal-hal seperti, katakanlah, bagian mana pun dari pustaka C ++ std ::, Anda kurang beruntung. Maaf, menyebalkan menjadi dirimu.
Code Abominator
43

Jika Anda menggunakan Boost 1.65 atau lebih tinggi, Anda dapat menggunakan boost :: stacktrace :

#include <boost/stacktrace.hpp>

// ... somewhere inside the bar(int) function that is called recursively:
std::cout << boost::stacktrace::stacktrace();
vasek
sumber
5
Dokumen boost menjelaskan tidak hanya menangkap jejak tumpukan, tetapi bagaimana melakukannya untuk pengecualian dan menegaskan. Hal yang bagus.
moodboom
1
Apakah stacktrace ini () mencetak file sumber dan nomor baris seperti yang diberikan dalam panduan GettingStarted?
Gimhani
11

Saya ingin menambahkan opsi pustaka standar (yaitu lintas-platform) cara membuat backtraces pengecualian, yang telah tersedia bersama C ++ 11 :

Gunakan std::nested_exceptiondanstd::throw_with_nested

Ini tidak akan memberi Anda tumpukan bersantai, tetapi menurut saya hal terbaik berikutnya. Dijelaskan pada StackOverflow di sini dan di sini , bagaimana Anda bisa mendapatkan backtrace pada pengecualian Anda di dalam kode Anda tanpa perlu debugger atau logging yang rumit, dengan hanya menulis handler pengecualian yang tepat yang akan mengumpulkan kembali pengecualian bersarang.

Karena Anda bisa melakukan ini dengan kelas pengecualian apa pun, Anda dapat menambahkan banyak informasi ke backtrace tersebut! Anda juga dapat melihat MWE saya di GitHub , di mana backtrace akan terlihat seperti ini:

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
sumber
Ini mungkin jauh lebih baik, jika Anda bersedia melakukan pekerjaan ekstra, daripada jejak tumpukan bodoh.
jelas
4

AFAIK libunwind cukup portabel dan sejauh ini saya belum menemukan sesuatu yang lebih mudah digunakan.

Nico Brailovsky
sumber
libunwind 1.1 tidak dibangun di atas os x.
xaxxon
4

Saya merekomendasikan proyek http://stacktrace.sourceforge.net/ . Ini mendukung Windows, Mac OS dan juga Linux

cahit beyaz
sumber
4
Di halaman beranda, saya melihat throw stack_runtime_error. Apakah saya benar dalam menyimpulkan bahwa lib ini hanya berfungsi untuk pengecualian yang berasal dari kelas itu, dan bukan untuk std::exceptionatau pengecualian dari perpustakaan pihak ketiga?
Thomas
4

Jika Anda menggunakan C ++ dan tidak ingin / tidak bisa menggunakan Boost, Anda dapat mencetak jejak balik dengan nama yang terurai menggunakan kode berikut [tautan ke situs asli] .

Catatan, solusi ini khusus untuk Linux. Menggunakan libc fungsi backtrace () / backtrace_symbols () (dari execinfo.h) GNU untuk mendapatkan backtrace dan kemudian menggunakan __cxa_demangle () (dari cxxabi.h) untuk mengubah nama simbol 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
sumber
3

Di Windows, lihat BugTrap . Ini tidak lagi di tautan asli, tetapi masih tersedia di CodeProject.

jww
sumber
3

Saya memiliki masalah yang sama, dan meskipun saya suka portabilitas, saya hanya perlu dukungan gcc. Di gcc, execinfo.h dan panggilan backtrace tersedia. Untuk menghilangkan nama fungsi, Mr. Bingmann memiliki kode yang bagus. Untuk membuang backtrace pada pengecualian, saya membuat pengecualian yang mencetak backtrace di konstruktor. Jika saya mengharapkan ini berfungsi dengan pengecualian yang dilemparkan ke perpustakaan, mungkin diperlukan pembangunan kembali / penautan sehingga pengecualian backtracing digunakan.

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

Mengkompilasi dan menjalankan ini dengan gcc 4.8.4 menghasilkan sebuah backtrace dengan nama fungsi C ++ yang tidak berubah:

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

Karena tumpukan sudah dicabut saat memasuki blok tangkap, solusi dalam kasus saya adalah tidak menangkap pengecualian tertentu yang kemudian mengarah ke SIGABRT. Dalam penangan sinyal untuk SIGABRT saya kemudian bercabang () dan execl () baik gdb (dalam build debug) atau stackwalk Google breakwalk (dalam build rilis). Saya juga mencoba hanya menggunakan fungsi aman pengendali sinyal.

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

Sunting: Untuk membuatnya berfungsi untuk breakpad saya juga harus menambahkan ini:

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

Sumber: Bagaimana cara mendapatkan jejak stack untuk C ++ menggunakan gcc dengan informasi nomor baris? dan Apakah mungkin untuk melampirkan gdb ke proses macet (alias debugging "just-in-time")

Bl00dh0und
sumber
2

Poppy tidak hanya dapat mengumpulkan jejak stack, tetapi juga nilai parameter, variabel lokal, dll. - segala sesuatu yang menyebabkan crash.

Orlin Georgiev
sumber
2

Kode berikut menghentikan eksekusi tepat setelah pengecualian dilemparkan. Anda perlu mengatur windows_exception_handler bersama dengan handler terminasi. Saya menguji ini dalam 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);
...
}

Periksa kode berikut untuk fungsi windows_exception_handler: http://www.codedisqus.com/0ziVPgVPUk/exception-handling-and-stacktrace-under-windows-mingwgcc.html

Marcos Fuentes
sumber
1

Cpp-tool ex_diag - easyweight, multiplatform, penggunaan sumber daya minimal, sederhana dan fleksibel saat dilacak.

Boris
sumber
Saya memeriksa proyek ini pada 2017.12.24, sumber dan unduhan keduanya tidak dapat diakses.
zhaorufei
1
Saya baru saja memeriksa code.google.com/archive/p/exception-diagnostic/source/default/… dan sumbernya dapat diunduh. Bisakah Anda mencoba sekali lagi?
Boris