C ++ แสดงการติดตามสแต็กบนข้อยกเว้น


204

ฉันต้องการมีวิธีรายงานการติดตามสแต็กกับผู้ใช้หากมีข้อผิดพลาดเกิดขึ้น วิธีที่ดีที่สุดในการทำเช่นนี้คืออะไร? ใช้รหัสพิเศษจำนวนมากหรือไม่

ในการตอบคำถาม:

ฉันต้องการให้พกพาได้ถ้าเป็นไปได้ ฉันต้องการข้อมูลที่จะปรากฏขึ้นเพื่อให้ผู้ใช้สามารถคัดลอกกองติดตามและส่งอีเมลถึงฉันหากมีข้อผิดพลาดเกิดขึ้น

คำตอบ:


76

มันขึ้นอยู่กับแพลตฟอร์มใด

ใน GCC มันค่อนข้างเล็กน้อยดูโพสต์นี้สำหรับรายละเอียดเพิ่มเติม

บน MSVC คุณสามารถใช้ไลบรารีStackWalkerที่จัดการการเรียก API พื้นฐานทั้งหมดที่จำเป็นสำหรับ Windows

คุณจะต้องหาวิธีที่ดีที่สุดในการรวมฟังก์ชั่นนี้เข้ากับแอพของคุณ แต่จำนวนของรหัสที่คุณต้องเขียนควรน้อยที่สุด


71
โพสต์คุณลิงก์ไปยังจุดส่วนใหญ่เพื่อสร้างการติดตามจาก segfault แต่ผู้ถามระบุเฉพาะข้อยกเว้นซึ่งเป็นสัตว์ที่แตกต่างกันมาก
Shep

8
ฉันเห็นด้วยกับ @Shep - คำตอบนี้ไม่ได้ช่วยในการติดตามสแต็คของรหัสการขว้างปาบน GCC ดูคำตอบของฉันสำหรับวิธีแก้ปัญหาที่เป็นไปได้
โทมัสเทมเพลมานน์

1
คำตอบนี้ทำให้เข้าใจผิด จุดเชื่อมโยงไปยังคำตอบที่เฉพาะเจาะจงเพื่อไม่Linux gcc
fjardon

คุณสามารถแทนที่กลไกการขว้างของlibstdc++(ใช้โดย GCC และเสียงดังกราว) ตามที่อธิบายไว้ในคำตอบนี้
ingomueller.net

59

คำตอบของ Andrew Grant ไม่ได้ช่วยในการติดตามสแต็คของฟังก์ชั่นการโยนอย่างน้อยไม่ได้กับ GCC เพราะคำสั่ง Throw ไม่ได้บันทึกการติดตามสแต็กปัจจุบันด้วยตนเองและตัวจัดการ catch จะไม่สามารถเข้าถึงการติดตามสแต็กที่ จุดนั้นอีกต่อไป

วิธีเดียวที่ใช้ GCC เพื่อแก้ปัญหานี้คือเพื่อให้แน่ใจว่าได้สร้างการติดตามสแต็ก ณ จุดของคำสั่งการโยนและบันทึกด้วยวัตถุข้อยกเว้น

แน่นอนว่าวิธีนี้ต้องการแน่นอนว่าทุกรหัสที่ใช้การยกเว้นจะใช้คลาส Exception นั้น

อัปเดต 11 กรกฎาคม 2560 : สำหรับโค้ดที่มีประโยชน์ลองดูที่คำตอบของ cahit beyaz ซึ่งชี้ไปที่http://stacktrace.sourceforge.net - ฉันยังไม่ได้ใช้ แต่มันก็ดูดี


1
น่าเสียดายที่ลิงค์นั้นเสีย คุณให้บริการอื่น ๆ ได้ไหม
warran

2
และ archive.org ก็ไม่ทราบเช่นกัน ประณาม. ดีขั้นตอนควรมีความชัดเจน: โยนวัตถุคลาสที่กำหนดเอง 'ที่บันทึกการติดตามสแต็คในเวลาของการโยน
Thomas Tempelmann

1
ในหน้าแรกของ StackTrace throw stack_runtime_errorฉันเห็น ฉันถูกต้องในการอนุมานว่า lib นี้ใช้ได้กับข้อยกเว้นที่ได้มาจากคลาสนั้นเท่านั้นและไม่ใช่std::exceptionหรือยกเว้นจากไลบรารีของบุคคลที่สาม
โทมัส

3
ดังนั้นคำตอบคือ "ไม่คุณไม่สามารถรับการติดตามสแต็กจากข้อยกเว้น C ++" ตัวเลือกเดียวคือการโยนคลาสของคุณเองซึ่งสร้างการติดตามสแต็กเมื่อสร้าง หากคุณติดอยู่กับสิ่งที่ต้องการพูดส่วนใดส่วนหนึ่งของไลบรารี C ++ std :: คุณไม่มีโชค ขอโทษนะที่เป็นคุณ
Code Abominator

43

หากคุณใช้ Boost 1.65 หรือสูงกว่าคุณสามารถใช้boost :: stacktrace :

#include <boost/stacktrace.hpp>

// ... somewhere inside the bar(int) function that is called recursively:
std::cout << boost::stacktrace::stacktrace();

5
เอกสารเพิ่มอธิบายไม่เพียง แต่จับกองติดตาม แต่วิธีการที่จะทำมันสำหรับข้อยกเว้นและอ้าง สิ่งที่ยอดเยี่ยม
moodboom

1
stacktrace () นี้พิมพ์ไฟล์ต้นฉบับและหมายเลขบรรทัดตามที่กำหนดในคู่มือ GettingStarted หรือไม่?
Gimhani


11

ฉันต้องการเพิ่มตัวเลือกไลบรารีมาตรฐาน (เช่นข้ามแพลตฟอร์ม) วิธีสร้าง backtraces ข้อยกเว้นซึ่งใช้ได้กับC ++ 11 :

ใช้std::nested_exceptionและstd::throw_with_nested

สิ่งนี้จะไม่ทำให้คุณรู้สึกผ่อนคลาย แต่ในความคิดของฉันสิ่งที่ดีที่สุดถัดไป มันอธิบายไว้ใน StackOverflow ที่นี่และที่นี่วิธีที่คุณจะได้รับ backtrace ในข้อยกเว้นของคุณในรหัสของคุณโดยไม่จำเป็นต้องมีการดีบักหรือการบันทึกที่ยุ่งยากโดยเพียงแค่เขียนตัวจัดการข้อยกเว้นที่เหมาะสมซึ่งจะสร้างข้อยกเว้นซ้อน

เนื่องจากคุณสามารถทำได้ด้วยคลาสยกเว้นที่ได้รับคุณสามารถเพิ่มข้อมูลจำนวนมากลงใน backtrace! คุณอาจจะดูMWEของฉันบน GitHubด้วยที่ backtrace จะมีหน้าตาแบบนี้:

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"

นี่น่าจะดีกว่ามากถ้าคุณเต็มใจทำงานพิเศษมากกว่าการติดตามสแต็กใบ้ปกติ
ชัดเจน

4

AFAIK libunwind เป็นแบบพกพาค่อนข้างมากและจนถึงตอนนี้ฉันไม่พบอะไรที่จะใช้ง่าย


libunwind 1.1 ไม่ได้สร้างบน os x
xaxxon

4

ฉันแนะนำโครงการhttp://stacktrace.sourceforge.net/ รองรับ Windows, Mac OS และ Linux


4
ฉันเห็นthrow stack_runtime_errorหน้าแรก ฉันถูกต้องในการอนุมานว่า lib นี้ใช้ได้เฉพาะกับข้อยกเว้นที่ได้รับจากคลาสนั้นและไม่ใช่std::exceptionหรือยกเว้นหรือจากไลบรารีบุคคลที่สาม
โทมัส

4

หากคุณใช้ C ++ และไม่ต้องการ / ไม่สามารถใช้ Boost คุณสามารถพิมพ์ติดตามย้อนหลังที่มีชื่อ demangled ใช้รหัสต่อไปนี้[เชื่อมโยงไปยังเว็บไซต์เดิม]

หมายเหตุโซลูชันนี้ใช้เฉพาะกับ Linux มันใช้ฟังก์ชั่น libc ของ GNU backtrace () / backtrace_symbols () (จาก execinfo.h) เพื่อรับ backtraces จากนั้นใช้ __cxa_demangle () (จาก cxxabi.h) เพื่อถอดชื่อสัญลักษณ์ 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!



3

บน Windows, ตรวจสอบBugTrap มันไม่ได้อยู่ที่ลิงค์เดิม แต่มันยังมีอยู่ใน CodeProject


3

ฉันมีปัญหาที่คล้ายกันและถึงแม้ว่าฉันชอบการพกพา แต่ฉันต้องการการสนับสนุน gcc เท่านั้น ใน gcc, execinfo.h และการโทรย้อนกลับมีให้บริการ ในการทำให้ชื่อฟังก์ชั่นมีความสับสนนาย Bingmann มีโค้ดที่ดี เมื่อต้องการทิ้ง backtrace บนข้อยกเว้นฉันสร้างข้อยกเว้นที่พิมพ์ backtrace ในตัวสร้าง หากฉันคาดหวังว่าสิ่งนี้จะทำงานได้โดยมีข้อยกเว้นเกิดขึ้นในไลบรารีมันอาจต้องมีการสร้างใหม่ / เชื่อมโยงเพื่อให้มีข้อยกเว้น backtracing

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

การคอมไพล์และรันด้วย gcc 4.8.4 ให้ผลย้อนหลังที่มีชื่อฟังก์ชั่น 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]

3

เนื่องจากสแต็กนั้นคลี่คลายแล้วเมื่อเข้าสู่ catch catch วิธีแก้ปัญหาในกรณีของฉันคือไม่จับข้อยกเว้นบางอย่างซึ่งนำไปสู่ ​​SIGABRT ในตัวจัดการสัญญาณสำหรับ SIGABRT ฉันก็ fork () และ execl () gdb (ใน debug builds) หรือ Google breakpads stackwalk (ใน build build) ฉันพยายามใช้ฟังก์ชั่นที่ปลอดภัยของตัวจัดการสัญญาณเท่านั้น

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

แก้ไข: เพื่อให้มันทำงานกับ breakpad ฉันต้องเพิ่มสิ่งนี้ด้วย:

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

ที่มา: วิธีรับการติดตามสแต็กสำหรับ C ++ โดยใช้ gcc พร้อมข้อมูลหมายเลขบรรทัด และเป็นไปได้หรือไม่ที่จะแนบ gdb เข้ากับกระบวนการที่ขัดข้อง


2

Poppyสามารถรวบรวมไม่เพียง แต่การติดตามสแต็ก แต่ยังรวมถึงค่าพารามิเตอร์ตัวแปรในเครื่องและอื่น ๆ - ทุกอย่างที่นำไปสู่ความผิดพลาด


2

รหัสต่อไปนี้หยุดการดำเนินการทันทีหลังจากมีข้อผิดพลาดเกิดขึ้น คุณต้องตั้ง windows_exception_handler พร้อมกับตัวจัดการการเลิกจ้าง ฉันทดสอบสิ่งนี้ใน 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);
...
}

ตรวจสอบรหัสต่อไปนี้สำหรับฟังก์ชั่น windows_exception_handler: http://www.codedisqus.com/0ziVPgVPUk/exception-handling-and-stacktrace-under-windows-mingwgcc.html


1

Cpp-tool ex_diag - น้ำหนักที่เบาง่ายหลากหลายแพลตฟอร์มใช้ทรัพยากรน้อยที่สุดเรียบง่ายและยืดหยุ่นตามร่องรอย


ฉันตรวจสอบโครงการนี้ที่ 2017.12.24 แหล่งที่มาและดาวน์โหลดทั้งสองไม่สามารถเข้าถึงได้
zhaorufei

1
ฉันเพิ่งตรวจสอบcode.google.com/archive/p/exception-diagnostic/source/default/…และแหล่งที่มานั้นสามารถดาวน์โหลดได้ คุณลองอีกครั้งได้ไหม
บอริส
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.