Як знайти, де було викинуто виняток у C ++?


92

У мене є програма, яка кидає десь невпійманий виняток. Все, що я отримую, - це повідомлення про кидок винятку і відсутність інформації про те, куди його було кинуто. Виглядає нелогічно, щоб програма, скомпільована, що містить символи налагодження, не повідомляла мене про те, де в моєму коді було створено виняток.

Чи є спосіб визначити, звідки виникають мої винятки, якщо не встановити `` catch throw '' у gdb та не викликати зворотну трасування для кожного окремого викинутого винятку?



Зробіть виняток і подивіться, що таке внутрішнє повідомлення. Оскільки доброю практикою є виняток, який походить від одного зі стандартних винятків (std :: runtime_error), ви повинні мати можливість його зловити за допомогою catch (std :: exception const & e)
Мартін Йорк

1
А std :: виняток / Std :: runtime_error вирішує проблему з'ясування "шляху" та походження винятку?
VolkerK

1
Оскільки у вашому запитанні вказано gdb, я думаю, що ваше рішення вже в SO: stackoverflow.com/questions/77005/... Я використав описане тут рішення, і воно чудово працює.
нейро

2
Вам слід подумати про те, щоб вказати ОС за допомогою тегу. Оскільки ви згадуєте gdb, я вважаю, що ви шукаєте рішення для Linux, а не Windows.
jschmier

Відповіді:


72

Ось деяка інформація, яка може бути корисною для налагодження вашої проблеми

Якщо виняток не зафіксовано, спеціальна функція бібліотеки std::terminate()автоматично викликається. Припинити це покажчик на функцію і значення по замовчуванням функції стандартної бібліотеки C std::abort(). Якщо немає прибирань не відбуваються за неперехваченное виняток , це може бути на самому справі корисно при налагодженні цієї проблеми не називається не руйнівники.
† Визначається реалізацією, чи розмотується стек перед викликом std::terminate().


Виклик abort()часто корисний для генерації дампа ядра, який можна проаналізувати, щоб визначити причину винятку. Переконайтеся, що ви ввімкнули дампи ядра через ulimit -c unlimited(Linux).


Ви можете встановити власну terminate()функцію за допомогою std::set_terminate(). Ви повинні мати можливість встановити точку зупинки для вашої функції завершення в gdb. Ви можете бути в змозі генерувати трасування стека з вашої terminate()функції , і це трасування може допомогти у визначенні місця розташування виключення.

Існує коротка дискусія щодо невпійманих винятків у « Мисленні Бруса Екеля» на C ++, 2-е видання, яка також може бути корисною.


Оскільки terminate()дзвінки abort()за замовчуванням (що спричинятиме SIGABRTсигнал за замовчуванням), можливо , ви зможете встановити SIGABRTобробник, а потім надрукувати зворотну трасу стека всередині обробника сигналу . Цей зворотний трас може допомогти у визначенні місця винятку.


Примітка: Я кажу, можливо, оскільки C ++ підтримує нелокальну обробку помилок за допомогою мовних конструкцій, щоб відокремити обробку помилок та код звітування від звичайного коду. Блок catch може бути, і часто знаходиться, в іншій функції / методі, ніж точка метання. Мені також було зазначено в коментарях (дякую Дену ), що визначається реалізацією, чи не розмотується стек до того, як terminate()буде викликано.

Оновлення: Я зібрав тестову програму Linux, яка називається, що генерує зворотне відстеження у 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 зловив непідключений виняток. what (): ПОМИЛКА RUNTIME!
зворотне відстеження my_terminate повернуло 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(основна+0xc1) [0x8049121]
[bt]: (8) ./test(__libc_start_main+0x95) [0x42017589]
[bt]: (9) ./test(__eh_alloc+0x3d) [0x8048b21]

сигнал 6 (перервано), адреса 0x1239 від 0x42029331
crit_err_hdlr зворотне відстеження повернуло 13 кадрів

[bt]: (1) ./test(kill+0x11) [0x42029331]
[bt]: (2) ./test(abort+0x16e) [0x4202a8c2]
[bt]: (3) ./ тест [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(основна+0xc1) [0x8049121]
[bt]: (11) ./test(__libc_start_main+0x95) [0x42017589]
[bt]: (12) ./test(__eh_alloc+0x3d) [0x8048b21]


1
Дуже цікаво. Я завжди підозрював, що не оброблений виняток буде розмотувати стек, поки він не дійде до найвищого рівня ( main), а потім викличе terminate(). Але ваш приклад показує, що розмотування взагалі не проводиться, що дуже класно.
Дан

6
1) throw(int)Специфікація непотрібна. 2) Можливо, uc->uc_mcontext.eipце дуже залежить від платформи (наприклад, використання ...ripна 64-бітній платформі). 3) Скомпілюйте, -rdynamicщоб отримати символи зворотного відстеження. 4) Біжіть, ./a.out 2>&1 | c++filtщоб отримати симпатичні зворотні символи.
Dan

2
"Жодні очищення не проводяться за незахопленим винятком." - Власне, це визначено реалізацією. Див. 15.3 / 9 та та 15.5.1 / 2 в специфікації C ++. "У ситуації, коли не знайдено жодного обробника, що відповідає, визначається реалізацією, чи розмотується стек перед викликом терміналу ()." Все-таки це чудове рішення, якщо ваш компілятор це підтримує!
Dan

1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;працював на мою ціль ARM
Стівен

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

Без подальшого ручного втручання це генерує безліч зворотних слідів, включаючи один для останнього вилученого винятку:

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,
Артем

7
Гаразд, я це виправив. Вибачте програміста, який працює як на Java, так і на C ++?
Ерік Германсен,

Поки я цим користувався. Проблема в тому, що він не говорить про те, що насправді спричинило виняток. Якщо, наприклад, у вас є 5 дзвінків стої в блоці try, ви не будете знати, який із них насправді винен.
Banjocat

5

Ви не передали інформацію про те, яку ОС / компілятор ви використовуєте.

У Visual Studio C ++ можуть бути передбачені винятки.

Див. "Інструментарій обробки винятків Visual C ++" на ddj.com

Моя стаття "Postmortem Debugging" , також на ddj.com, включає код для використання структурованої обробки винятків Win32 (використовується інструментарієм) для ведення журналу тощо.


він сказав gdb, що майже виключає Windows / Visual Studio.
Бен Войгт,

2
Ну, він каже, що хотів би чогось, "що не відповідає gdb", але явно не посилається на жодну ОС / компілятор. У цьому проблема людей, які не заявляють про такі речі.
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 припускає, що в libgcc є функція _Unwind_Backtrace, яка, ймовірно, є частиною того, що вам потрібно.


Можливо, тому, що "дайте мені знати, якщо ви хочете контур" - це не корисна відповідь. Але _Unwind_Backtrace є; компенсуючий.
Томас

Виходячи з того, що в OP згадується gdb, я здогадався, що Windows не має значення. Алекс, звичайно, міг вільно редагувати своє запитання, сказавши Windows.
Бен Войгт,

1

Перевірте цю тему, можливо, це допоможе:

Виловлювання всіх необроблених винятків C ++?

Я отримав хороший досвід роботи з цим програмним забезпеченням:

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

Він може роздрукувати трасування стека у файл для будь-якого необробленого винятку.


Я думаю, справа в тому, що Алекс хоче стек-трас, як exception thrown foo.c@54, ..., re-thrown bar.c@54, ....без необхідності робити це вручну.
VolkerK
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.