C ++에서 예외가 발생한 위치를 어떻게 찾습니까?


95

어딘가에 잡히지 않은 예외를 던지는 프로그램이 있습니다. 내가 얻는 것은 예외가 발생했다는보고 뿐이며 예외가 발생 된 위치에 대한 정보는 없습니다. 내 코드에서 예외가 생성 된 위치를 알려주지 않는 디버그 기호를 포함하도록 컴파일 된 프로그램이 비논리적으로 보입니다.

gdb에서 'catch throw'를 설정하고 throw되는 모든 예외에 대해 역 추적을 호출하지 않고 내 예외가 어디에서 오는지 알 수있는 방법이 있습니까?



예외를 포착하고 내부 메시지가 무엇인지 확인하십시오. 예외가 표준 예외 (std :: runtime_error) 중 하나에서 파생되는 것이 좋은 관행이므로 catch (std :: exception const & e)
Martin York

1
그리고 std :: exception / Std :: runtime_error는 "경로"와 예외의 원인을 찾는 문제를 해결합니까?
VolkerK

1
귀하의 질문에 gdb가 있으므로 귀하의 솔루션이 이미 SO에 있다고 생각합니다 : stackoverflow.com/questions/77005/… 여기에 설명 된 솔루션을 사용했으며 완벽하게 작동합니다.
뉴로

2
태그를 통해 OS를 지정하는 것을 고려해야합니다. gdb를 언급 했으므로 Windows가 아닌 Linux 솔루션을 찾고 있다고 가정합니다.
jschmier

답변:


73

다음 은 문제를 디버깅하는 데 사용할 있는 몇 가지 정보입니다.

예외가 포착되지 않으면 특수 라이브러리 함수 std::terminate()가 자동으로 호출됩니다. Terminate는 실제로 함수에 대한 포인터이며 기본값은 표준 C 라이브러리 함수 std::abort()입니다. 더 정리가 잡히지 않은 예외 발생하지 않으면 , 그것은 실제로 소멸자가 호출되지 않습니다으로이 문제를 디버깅하는 데 도움이 될.
† 스택 std::terminate()이 호출 되기 전에 풀 렸는지 여부는 구현에서 정의됩니다 .


에 대한 호출 abort()은 예외의 원인을 확인하기 위해 분석 할 수있는 코어 덤프를 생성하는 데 유용합니다. ulimit -c unlimited(Linux) 를 통해 코어 덤프를 활성화했는지 확인하십시오 .


을 사용하여 자신 만의 terminate()기능을 설치할 수 있습니다 std::set_terminate(). gdb에서 종료 함수에 중단 점을 설정할 수 있어야합니다. 당신은 할 수 귀하의에서 스택 역 추적을 생성 할 수 terminate()기능이 역 추적을 할 수 있습니다 예외의 위치를 식별하는 데 도움이.

Bruce Eckel의 Thinking in C ++, 2nd Ed에서도 잡히지 않은 예외 에 대한 간단한 토론 이 있습니다.


이후 terminate()통화 abort()기본 (A 발생할 수있는 SIGABRT기본적으로 신호)를, 당신은 할 수있다 세트 할 수 SIGABRT처리기를 다음 신호 처리기 내에서 스택 역 추적을 인쇄 . 이 역 추적 예외 위치를 식별하는 데 도움 이 될 수 있습니다 .


참고 : 내가 말할 수도 C ++ 지원 로컬이 아닌 오류가 별도의 오류 처리에 대한 언어 구조의 사용을 통해 처리하고 일반 코드에서 코드를보고 있기 때문이다. 캐치 블록은 던지는 지점과 다른 기능 / 방법에 위치 할 수 있으며 종종 있습니다. 또한 주석에서 ( Dan 에게 감사드립니다 ) 스택 terminate()이 호출 되기 전에 풀 렸는지 여부에 관계없이 구현 정의 되어 있음을 지적했습니다 .

업데이트 :terminate() 를 통해 설정된 함수 set_terminate()와 .NET 용 신호 처리기에서 다른 함수 세트 에서 역 추적을 생성하는 Linux 테스트 프로그램을 함께 던졌습니다 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 () : 런타임 오류!
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 (중단됨), 주소는 0x42029331에서 0x1239입니다.
crit_err_hdlr 역 추적이 13 프레임을 반환했습니다.

[bt] : (1) ./test(kill+0x11) [0x42029331]
[bt] : (2) ./test (중단 + 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
매우 흥미로운. 나는 항상 최고 수준 (에 도착 할 때까지 처리되지 않은 예외가 스택 긴장을 풀 것이라고 의심 main)와 다음 이 부를 것이다 terminate(). 그러나 귀하의 예는 풀기가 전혀 수행되지 않음을 보여줍니다.
Dan

6
1) throw(int)사양은 불필요합니다. 2) uc->uc_mcontext.eip아마도 매우 플랫폼 의존적 일 것입니다 (예 : ...rip64 비트 플랫폼에서 사용). 3) 컴파일하여 -rdynamic역 추적 기호를 얻습니다. 4) ./a.out 2>&1 | c++filt예쁜 역 추적 기호를 얻으려면 실행하십시오 .
Dan

2
"발견되지 않은 예외에 대한 정리가 발생하지 않습니다." -실제로는 구현에 따라 정의됩니다. C ++ 사양의 15.3 / 9 및 15.5.1 / 2를 참조하세요. "일치하는 핸들러가없는 상황에서, terminate ()가 호출되기 전에 스택이 풀 렸는지 여부는 구현에서 정의됩니다." 그래도 컴파일러가 지원한다면 이것은 훌륭한 솔루션입니다!
Dan

1
((sig_ucontext_t *)userContext)->uc_mcontext.fault_address;내 ARM의 대상에 대해 일
스티븐

1
몇 가지 참고 사항 : backtrace_symbols ()는 malloc을 수행하므로 시작시 메모리 블록을 미리 할당 한 다음 발생하는 경우 backtrace_symbols ()를 호출하기 직전에 my_terminate ()에서 할당을 취소 할 수 있습니다. std :: bad_alloc () 예외 처리. 또한, <cxxabi.h>를 포함시킨 다음 __cxa_demangle ()을 사용하여 출력 메시지 [] 문자열에서 '('와 '+'사이에 표시되는 잘린 부분 문자열을 유용하게 만들 수 있습니다.
K Scott Piel

51

당신이 말했듯이, 우리는 gdb에서 'catch throw'를 사용하고 throw되는 모든 예외에 대해 '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 ++는 자바가 아닙니다.
Artyom

7
알겠습니다. 수정했습니다. Java와 C ++에서 모두 작동하는 프로그래머를 용서 하시겠습니까?
Erik Hermansen

내가 이것을 사용하는 동안. 문제는 실제로 예외를 던진 것이 무엇인지 알려주지 않는다는 것입니다. 예를 들어 try 블록에 5 개의 stoi 호출이있는 경우 실제로 어떤 것이 범인인지 알 수 없습니다.
Banjocat 2015 년

5

사용하는 OS / 컴파일러에 대한 정보를 전달하지 않았습니다.

Visual Studio C ++에서 예외를 계측 할 수 있습니다.

참조 "비주얼 C ++ 예외 처리 계측을" ddj.com에

ddj.com의 내 기사 " Postmortem Debugging" 에는 로깅 등을 위해 Win32 구조적 예외 처리 (계측에서 사용)를 사용하는 코드가 포함되어 있습니다.


그는 Windows / Visual Studio를 거의 배제하는 gdb라고 말했습니다.
Ben Voigt

2
그는 "gdb가 부족한"것을 원한다고 말하지만 명시 적으로 OS / 컴파일러를 언급하지는 않습니다. 그것이 사람들이 그러한 것들을 선언하지 않는 문제입니다.
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는 다음과 같습니다. 보상.
Thomas

OP가 gdb를 언급했음을 기반으로 Windows가 관련이 없다고 생각했습니다. 물론 Alex는 Windows에 대한 질문을 자유롭게 편집 할 수있었습니다.
Ben Voigt

1

이 스레드를 확인하면 도움이 될 것입니다.

처리되지 않은 모든 C ++ 예외 잡기?

나는 그 소프트웨어로 좋은 경험을했습니다.

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

처리되지 않은 예외에 대해 파일에 스택 추적을 인쇄 할 수 있습니다.


요점은 Alex가 exception thrown foo.c@54, ..., re-thrown bar.c@54, ....수동으로 수행하지 않고도 스택 추적을 원한다는 것입니다.
VolkerK
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.