ฉันจะหาข้อยกเว้นใน C ++ ได้อย่างไร


97

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

มีวิธีใดบ้างที่จะบอกได้ว่าข้อยกเว้นของฉันมาจากการไม่ตั้งค่า 'catch throw' ใน gdb และเรียก backtrace สำหรับข้อยกเว้นการโยนทุกครั้ง



จับข้อยกเว้นและดูว่าข้อความภายในคืออะไร เนื่องจากเป็นแนวทางปฏิบัติที่ดีสำหรับข้อยกเว้นที่จะได้รับจากข้อยกเว้นมาตรฐานข้อใดข้อหนึ่ง (std :: runtime_error) คุณจึงสามารถจับได้ด้วยการจับ (std :: ข้อยกเว้น const & e)
Martin York

1
และ std :: exception / Std :: runtime_error แก้ปัญหาในการค้นหา "เส้นทาง" และที่มาของข้อยกเว้นหรือไม่
VolkerK

1
ในสถานะคำถามของคุณ gdb ฉันคิดว่าโซลูชันของคุณอยู่ใน SO แล้ว: stackoverflow.com/questions/77005/… ฉันได้ใช้วิธีแก้ปัญหาที่อธิบายไว้ที่นี่และทำงานได้อย่างสมบูรณ์
neuro

2
คุณควรพิจารณาระบุ OS ผ่านแท็ก เนื่องจากคุณพูดถึง gdb ฉันคิดว่าคุณกำลังมองหาโซลูชัน Linux ไม่ใช่ Windows
jschmier

คำตอบ:


74

นี่คือข้อมูลบางส่วนที่อาจใช้ในการแก้ไขปัญหาของคุณ

หากไม่มีข้อยกเว้นฟังก์ชันไลบรารีพิเศษstd::terminate()จะถูกเรียกโดยอัตโนมัติ std::abort()ยุติเป็นจริงชี้ไปยังฟังก์ชั่นและการเริ่มต้นค่าฟังก์ชั่นมาตรฐานห้องสมุด C หากไม่มีการสะสางเกิดขึ้นสำหรับข้อยกเว้น uncaught มันอาจจะจริงจะเป็นประโยชน์ในการแก้จุดบกพร่องปัญหานี้ไม่มี destructors จะเรียกว่า
†มีการกำหนดการใช้งานไม่ว่าจะคลายสแต็กก่อนที่จะstd::terminate()ถูกเรียกหรือไม่


การเรียกหาabort()มักมีประโยชน์ในการสร้างคอร์ดัมพ์ที่สามารถวิเคราะห์เพื่อหาสาเหตุของข้อยกเว้น ตรวจสอบให้แน่ใจว่าคุณเปิดใช้งานการถ่ายโอนข้อมูลหลักผ่านulimit -c unlimited(Linux)


คุณสามารถติดตั้งterminate()ฟังก์ชันของคุณเองโดยใช้std::set_terminate(). คุณควรตั้งค่าเบรกพอยต์บนฟังก์ชันยุติใน gdb ได้ คุณอาจสร้าง stack backtrace จากterminate()ฟังก์ชันของคุณได้และ backtrace นี้อาจช่วยในการระบุตำแหน่งของข้อยกเว้น

มีการอภิปรายสั้น ๆ เกี่ยวกับข้อยกเว้นที่ไม่ถูกจับได้ในThinking in C ++, 2nd Ed ของ Bruce Eckelที่อาจเป็นประโยชน์เช่นกัน


ตั้งแต่terminate()สายabort()โดยค่าเริ่มต้น (ซึ่งจะทำให้เกิดSIGABRTสัญญาณโดยค่าเริ่มต้น), คุณอาจจะสามารถตั้งค่าการSIGABRTจัดการและจากนั้นพิมพ์ backtrace สแต็คจากภายในจัดการสัญญาณ การติดตามย้อนกลับนี้อาจช่วยในการระบุตำแหน่งของข้อยกเว้น


หมายเหตุ:ฉันพูดว่าอาจเป็นเพราะ C ++ สนับสนุนการจัดการข้อผิดพลาดที่ไม่ใช่ในเครื่องผ่านการใช้โครงสร้างภาษาเพื่อแยกการจัดการข้อผิดพลาดและรหัสการรายงานออกจากรหัสธรรมดา บล็อกจับได้และมักจะอยู่ในฟังก์ชัน / วิธีการที่แตกต่างจากจุดที่ขว้างปา นอกจากนี้ยังมีการชี้ให้ฉันเห็นในความคิดเห็น (ขอบคุณDan ) ว่ามีการกำหนดการใช้งานไม่ว่าจะคลายสแต็กก่อนที่จะterminate()ถูกเรียก

อัปเดต:ฉันรวบรวมโปรแกรมทดสอบ Linux ที่เรียกว่าสร้าง backtrace ในterminate()ชุดฟังก์ชันผ่านset_terminate()และอีกโปรแกรมหนึ่งในตัวจัดการสัญญาณสำหรับSIGABRT. การย้อนกลับทั้งสองแสดงตำแหน่งของข้อยกเว้นที่ไม่สามารถจัดการได้อย่างถูกต้อง

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

รหัส:

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

เอาท์พุต:

my_terminate พบข้อยกเว้นที่ไม่ได้จัดการ อะไร (): RUNTIME ERROR!
my_terminate backtrace ส่งคืน 10 เฟรม

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

สัญญาณ 6 (ยกเลิก) ที่อยู่คือ 0x1239 จาก 0x42029331
Crit_err_hdlr backtrace ส่งคืน 13 เฟรม

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


1
น่าสนใจมาก. ฉันมักจะสงสัยว่ามีข้อยกเว้น unhandled จะผ่อนคลายสแต็คจนกว่าจะมีการระดับบนสุด ( main) และจากนั้นterminate()ก็จะเรียก แต่ตัวอย่างของคุณแสดงให้เห็นว่าไม่มีการคลี่คลายเลยซึ่งเป็นเรื่องที่ดี
แดน

6
1) throw(int)ข้อมูลจำเพาะไม่จำเป็น 2) uc->uc_mcontext.eipอาจขึ้นอยู่กับแพลตฟอร์มมาก (เช่นใช้...ripบนแพลตฟอร์ม 64 บิต) 3) รวบรวม-rdynamicเพื่อให้คุณได้รับสัญลักษณ์ย้อนกลับ 4) วิ่ง./a.out 2>&1 | c++filtเพื่อรับสัญลักษณ์ backtrace สวย ๆ
แดน

2
"ไม่มีการล้างข้อมูลใด ๆ เกิดขึ้นสำหรับข้อยกเว้นที่ไม่ถูกจับ" - ตามความเป็นจริงนั่นคือการกำหนดการนำไปใช้งาน ดู 15.3 / 9 และ 15.5.1 / 2 ในข้อกำหนด C ++ "ในสถานการณ์ที่ไม่พบตัวจัดการที่ตรงกันจะมีการกำหนดการใช้งานไม่ว่าจะคลายสแต็กก่อนที่จะเรียก terminate () หรือไม่" ถึงกระนั้นนี่เป็นวิธีแก้ปัญหาที่ยอดเยี่ยมหากคอมไพเลอร์ของคุณรองรับ!
แดน

1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;ทำงานให้กับเป้าหมาย ARM ของฉัน
stephen

1
บันทึกสองสามข้อ: backtrace_symbols () ทำ malloc ... ดังนั้นคุณอาจต้องการจัดสรรบล็อกหน่วยความจำล่วงหน้าเมื่อเริ่มต้นจากนั้นยกเลิกการจัดสรรใน my_terminate () ก่อนที่จะเรียก backtrace_symbols () ในกรณีที่คุณเป็น การจัดการข้อยกเว้น std :: bad_alloc () นอกจากนี้คุณสามารถรวม <cxxabi.h> แล้วใช้ __cxa_demangle () เพื่อสร้างสิ่งที่เป็นประโยชน์จากสตริงย่อยที่ถูกแยกออกซึ่งแสดงอยู่ระหว่าง '(' และ '+' ในสตริงเอาต์พุต []
K Scott Piel

51

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

gdb>

set pagination off
catch throw
commands
backtrace
continue
end
run

หากไม่มีการแทรกแซงด้วยตนเองเพิ่มเติมสิ่งนี้จะสร้าง backtraces จำนวนมากรวมถึงหนึ่งสำหรับข้อยกเว้นสุดท้ายที่ไม่ถูกจับ:

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.

นี่คือบล็อกโพสต์ที่ยอดเยี่ยมที่รวบรวมไว้: http://741mhz.com/throw-stacktrace [ใน archive.org]


17

คุณสามารถสร้างมาโครเช่น:

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

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


18
-1 คุณทำไม่ได้throw new excation(...)แต่throw exception(...)C ++ ไม่ใช่ Java
Artyom

7
โอเคฉันแก้ไขแล้ว ให้อภัยโปรแกรมเมอร์ที่ทำงานได้ทั้งใน Java และ C ++ หรือไม่?
Erik Hermansen

ในขณะที่ฉันได้ใช้สิ่งนี้ ปัญหาของมันคือมันไม่ได้บอกสิ่งที่เกิดข้อยกเว้น ตัวอย่างเช่นคุณมีการโทร 5 ครั้งใน try block คุณจะไม่รู้ว่าอันไหนเป็นผู้กระทำผิด
Banjocat

5

คุณไม่ได้ส่งข้อมูลเกี่ยวกับระบบปฏิบัติการ / คอมไพเลอร์ที่คุณใช้

ใน Visual Studio C ++ ข้อยกเว้นสามารถเป็นเครื่องมือได้

ดู "Visual C ++ Exception-Handling Instrumentation"บน ddj.com

บทความของฉัน"Postmortem Debugging"ใน ddj.com ยังมีโค้ดสำหรับใช้การจัดการข้อยกเว้นที่มีโครงสร้าง Win32 (ใช้โดยเครื่องมือวัด) สำหรับการบันทึกเป็นต้น


เขากล่าวว่า gdb ซึ่งค่อนข้างจะออกกฎ Windows / Visual Studio
Ben Voigt

2
เขาบอกว่าเขาต้องการอะไรที่ "สั้น ๆ ของ gdb" แต่เขาไม่ได้อ้างถึง OS / Compiler ใด ๆ อย่างชัดเจน นั่นคือปัญหาของผู้คนที่ไม่ประกาศสิ่งดังกล่าว
RED SOFT ADAIR

5

คุณสามารถทำเครื่องหมายตำแหน่งที่แน่นหลักในรหัสของคุณnoexceptเพื่อค้นหาข้อยกเว้นจากนั้นใช้libunwind (เพียงเพิ่ม-lunwindในพารามิเตอร์ตัวเชื่อมโยง) (ทดสอบด้วย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
}

}

มีบทความที่ดีเกี่ยวกับปัญหานี้


1

ฉันมีโค้ดสำหรับทำสิ่งนี้ใน Windows / Visual Studio โปรดแจ้งให้เราทราบหากคุณต้องการร่าง ไม่รู้จะทำอย่างไรสำหรับรหัส dwarf2 แต่ Google แนะนำว่ามีฟังก์ชัน _Unwind_Backtrace ใน libgcc ซึ่งอาจเป็นส่วนหนึ่งของสิ่งที่คุณต้องการ


อาจเป็นเพราะ "แจ้งให้เราทราบหากคุณต้องการโครงร่าง" ไม่ใช่คำตอบที่มีประโยชน์ แต่ _Unwind_Backtrace คือ; ชดเชย.
Thomas

บนพื้นฐานที่ OP กล่าวถึง gdb ฉันเดาว่า Windows ไม่เกี่ยวข้อง แน่นอนว่าอเล็กซ์มีอิสระที่จะแก้ไขคำถามของเขาเพื่อพูดว่า Windows
Ben Voigt

1

ตรวจสอบหัวข้อนี้อาจช่วยได้:

จับข้อยกเว้น C ++ ที่ไม่สามารถจัดการได้ทั้งหมดหรือไม่?

ฉันสร้างประสบการณ์ที่ดีกับซอฟต์แวร์นั้น:

http://www.codeproject.com/KB/applications/blackbox.aspx

สามารถพิมพ์การติดตามสแต็กไปยังไฟล์สำหรับข้อยกเว้นที่ไม่สามารถจัดการได้


ฉันคิดว่าประเด็นคือ Alex ต้องการ stacktrace exception thrown foo.c@54, ..., re-thrown bar.c@54, ....โดยไม่ต้องทำด้วยตนเอง
VolkerK
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.