Bagaimana cara menemukan tempat pengecualian dilemparkan di C ++?

97

Saya memiliki program yang menampilkan pengecualian yang tidak tertangkap di suatu tempat. Yang saya dapatkan hanyalah laporan tentang pengecualian yang dilemparkan, dan tidak ada informasi ke mana itu dilemparkan. Tampaknya tidak masuk akal untuk program yang dikompilasi berisi simbol debug untuk tidak memberi tahu saya di mana pengecualian kode saya dibuat.

Apakah ada cara untuk mengetahui di mana pengecualian saya berasal dari kekurangan pengaturan 'catch throw' di gdb dan memanggil backtrace untuk setiap pengecualian yang dilempar?

Alex
sumber
Tangkap pengecualian dan lihat apa pesan internalnya. Karena ini adalah praktik yang baik untuk pengecualian diturunkan dari salah satu pengecualian standar (std :: runtime_error) Anda harus dapat menangkapnya dengan catch (std :: exception const & e)
Martin York
1
Dan std :: exception / Std :: runtime_error memecahkan masalah menemukan "jalur" dan asal pengecualian?
VolkerK
1
Sebagai status pertanyaan Anda gdb, saya pikir solusi Anda sudah ada di SO: stackoverflow.com/questions/77005/… Saya telah menggunakan solusi yang dijelaskan di sini dan berfungsi dengan sempurna.
neuro
2
Anda harus mempertimbangkan untuk menentukan OS melalui tag. Karena Anda menyebutkan gdb, saya anggap Anda sedang mencari solusi Linux dan bukan Windows.
jschmier

Jawaban:

74

Berikut beberapa info yang mungkin berguna untuk men-debug masalah Anda

Jika pengecualian tidak tertangkap, fungsi perpustakaan khusus std::terminate()dipanggil secara otomatis. Hentikan sebenarnya adalah penunjuk ke fungsi dan nilai defaultnya adalah fungsi pustaka C Standar std::abort(). Jika tidak ada pembersihan terjadi karena eksepsi tidak tertangkap , itu mungkin benar-benar membantu dalam debugging masalah ini karena tidak ada destructors disebut.
† Ini adalah implementasi-ditentukan apakah tumpukan dibatalkan sebelum std::terminate()dipanggil.


Panggilan ke abort()sering kali berguna dalam menghasilkan dump inti yang dapat dianalisis untuk menentukan penyebab pengecualian. Pastikan Anda mengaktifkan core dump melalui ulimit -c unlimited(Linux).


Anda dapat menginstal terminate()fungsi Anda sendiri dengan menggunakan std::set_terminate(). Anda harus bisa mengatur breakpoint pada fungsi terminate Anda di gdb. Anda mungkin dapat membuat pelacakan mundur tumpukan dari terminate()fungsi Anda dan pelacakan mundur ini dapat membantu dalam mengidentifikasi lokasi pengecualian.

Ada diskusi singkat tentang pengecualian yang tidak tertangkap dalam Bruce Eckel's Thinking in C ++, 2nd Ed yang mungkin bisa membantu juga.


Karena terminate()panggilan abort()secara default (yang akan menyebabkan SIGABRTsinyal secara default), Anda mungkin dapat mengatur SIGABRTpenangan dan kemudian mencetak pelacakan mundur tumpukan dari dalam penangan sinyal . Pelacakan balik ini dapat membantu dalam mengidentifikasi lokasi pengecualian.


Catatan: Saya katakan mungkin karena C ++ mendukung penanganan kesalahan non-lokal melalui penggunaan konstruksi bahasa untuk memisahkan penanganan kesalahan dan kode pelaporan dari kode biasa. Blok tangkapan dapat, dan sering kali, terletak di fungsi / metode yang berbeda dari titik lemparan. Itu juga telah ditunjukkan kepada saya di komentar (terima kasih Dan ) bahwa itu adalah implementasi-ditentukan apakah tumpukan dibatalkan sebelum terminate()dipanggil.

Pembaruan: Saya mengumpulkan program uji Linux yang disebut yang menghasilkan pelacakan balik dalam terminate()fungsi yang ditetapkan melalui set_terminate()dan lainnya dalam penangan sinyal untuk SIGABRT. Kedua jejak belakang dengan benar menunjukkan lokasi pengecualian tidak tertangani.

Pembaruan 2: Berkat posting blog tentang Menangkap pengecualian yang tidak tertangkap dalam penghentian , saya belajar beberapa trik baru; termasuk membuang kembali pengecualian yang tidak tertangkap dalam penangan terminate. Penting untuk diperhatikan bahwa throwpernyataan kosong dalam penangan penghentian khusus berfungsi dengan GCC dan bukan solusi portabel.

Kode:

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

Keluaran:

my_terminate menangkap pengecualian tanpa tangan. apa (): ERROR RUNTIME!
my_terminate backtrace mengembalikan 10 frame

[bt]: (0) ./test(terminasi_saya__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]

sinyal 6 (Dibatalkan), alamatnya adalah 0x1239 dari 0x42029331
crit_err_hdlr backtrace menghasilkan 13 frame

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

jschmier.dll
sumber
1
Sangat menarik. Saya selalu curiga bahwa pengecualian yang tidak tertangani akan melepas tumpukan hingga mencapai level teratas ( main) dan kemudian akan dipanggil terminate(). Tapi contoh Anda menunjukkan bahwa tidak ada relaksasi sama sekali, yang sangat keren.
Dan
6
1) throw(int)Spesifikasi tidak diperlukan. 2) uc->uc_mcontext.eipMungkin sangat bergantung pada platform (misalnya, digunakan ...rippada platform 64-bit). 3) Kompilasi dengan -rdynamicsehingga Anda mendapatkan simbol backtrace. 4) Jalankan ./a.out 2>&1 | c++filtuntuk mendapatkan simbol lacak balik yang cantik.
Dan
2
"Tidak ada pembersihan yang terjadi untuk pengecualian yang tidak tertangkap." - Sebenarnya, itu adalah definisi implementasi. Lihat 15.3 / 9 dan dan 15.5.1 / 2 di spesifikasi C ++. "Dalam situasi di mana tidak ada penangan yang cocok ditemukan, itu adalah implementasi-ditentukan apakah tumpukan dibatalkan atau tidak sebelum terminate () dipanggil." Namun, ini adalah solusi yang bagus jika kompiler Anda mendukungnya!
Dan
1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;bekerja untuk sasaran ARM saya
stephen
1
Beberapa catatan: backtrace_symbols () melakukan malloc ... jadi, Anda mungkin ingin mengalokasikan blok memori sebelumnya saat startup, kemudian membatalkan alokasinya di my_terminate () tepat sebelum memanggil backtrace_symbols () jika Anda kebetulan menangani pengecualian std :: bad_alloc (). Selain itu, Anda dapat menyertakan <cxxabi.h> dan kemudian menggunakan __cxa_demangle () untuk membuat sesuatu yang berguna dari substring rusak yang ditampilkan di antara string '(' dan '+' dalam pesan keluaran [].
K Scott Piel
51

Seperti yang Anda katakan, kita bisa menggunakan 'catch throw' di gdb dan memanggil 'backtrace' untuk setiap pengecualian yang dilempar. Meskipun biasanya terlalu membosankan untuk dilakukan secara manual, gdb memungkinkan otomatisasi proses. Itu memungkinkan melihat lacak balik dari semua pengecualian yang muncul, termasuk yang terakhir yang tidak tertangkap:

gdb>

set pagination off
catch throw
commands
backtrace
continue
end
run

Tanpa intervensi manual lebih lanjut, ini menghasilkan banyak jejak balik, termasuk satu untuk pengecualian terakhir yang tidak tertangkap:

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.

Berikut entri blog bagus yang merangkumnya : http://741mhz.com/throw-stacktrace [di archive.org]

TimJ
sumber
17

Anda dapat membuat makro seperti:

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

... dan ini akan memberi Anda lokasi tempat pengecualian dilemparkan (memang bukan pelacakan tumpukan). Anda perlu mendapatkan pengecualian dari beberapa kelas dasar yang menggunakan konstruktor di atas.

Erik Hermansen
sumber
18
-1 Anda tidak throw new excation(...)tetapi throw exception(...)C ++ bukan Java,
Artyom
7
Oke, sudah saya perbaiki. Maafkan programmer yang bekerja di Java dan C ++ mungkin?
Erik Hermansen
Sementara saya telah menggunakan ini. Masalahnya adalah ia tidak memberi tahu apa yang sebenarnya membuat pengecualian. Jika misalnya Anda memiliki 5 panggilan stoi dalam blok percobaan, Anda tidak akan tahu mana yang sebenarnya pelakunya.
Banjocat
5

Anda tidak menyampaikan informasi tentang OS / Compiler yang Anda gunakan.

Dalam Visual Studio C ++ pengecualian dapat diinstrumentasi.

Lihat "Instrumentasi Penanganan Pengecualian C ++ Visual" di ddj.com

Artikel saya "Postmortem Debugging" , juga di ddj.com menyertakan kode untuk menggunakan penanganan pengecualian terstruktur Win32 (digunakan oleh instrumentasi) untuk logging dll.

ADAIR LEMBUT MERAH
sumber
katanya gdb, yang cukup banyak mengesampingkan Windows / Visual Studio.
Ben Voigt
2
Dia bilang dia ingin sesuatu yang "short of gdb", tapi dia tidak secara eksplisit merujuk ke OS / Compiler. Itulah masalah orang tidak mengumumkan hal seperti itu.
RED SOFT ADAIR
5

Anda dapat menandai tempat sempit utama dalam kode Anda noexceptuntuk menemukan pengecualian, lalu gunakan libunwind (cukup tambahkan -lunwindke parameter linker) (diuji dengan 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
}

}

Ada artikel bagus tentang masalah ini.

Tomilov Anatoliy
sumber
1

Saya punya kode untuk melakukan ini di Windows / Visual Studio, beri tahu saya jika Anda menginginkan garis besar. Tidak tahu bagaimana melakukannya untuk kode dwarf2, Google cepat menyarankan bahwa ada fungsi _Unwind_Backtrace di libgcc yang mungkin merupakan bagian dari apa yang Anda butuhkan.

Ben Voigt
sumber
Mungkin karena "beri tahu saya jika Anda menginginkan garis besar" bukanlah jawaban yang berguna. Tapi _Unwind_Backtrace adalah; kompensasi.
Thomas
Atas dasar OP menyebutkan gdb, saya kira Windows tidak relevan. Alex, tentu saja, bebas mengedit pertanyaannya untuk mengatakan Windows.
Ben Voigt