C++ - Backtrace and Exception handling

  • 先看一个演示程序,这个程序会打印出当前点的调用堆栈:
#include <iostream>
#include <vector>
#include <string>
#include <Windows.h>
#include <dbghelp.h>
 
#pragma comment(lib, "Dbghelp.lib")
 
std::vector<std::string> capture_stack_backtrace(int max_frames)
{
    auto backtrace = (void **)malloc(sizeof(void *) * max_frames);
    auto nFrames = ::CaptureStackBackTrace(0, max_frames, backtrace, NULL);
 
    auto hProc = ::GetCurrentProcess();
    ::SymInitialize(hProc, NULL, TRUE);
 
    auto sym = (SYMBOL_INFO *)malloc(sizeof(SYMBOL_INFO) + (1024 - 1) * sizeof(TCHAR));
    sym->SizeOfStruct = sizeof(SYMBOL_INFO);
    sym->MaxNameLen = 1024;
 
    IMAGEHLP_LINE line;
    line.SizeOfStruct = sizeof(IMAGEHLP_LINE);
 
    DWORD displacement;
    std::vector<std::string> result;
 
    char buf[2048];
 
    for (int i = 0; i < nFrames; i++) {
        auto addr = backtrace[i];
        ::SymFromAddr(hProc, (DWORD64)addr, NULL, sym);
        if (::SymGetLineFromAddr(hProc, (DWORD64)addr, &displacement, &line)) {
            sprintf_s(buf, "%d: at %s (0x%p)\n\tin %s(%d)", i, sym->Name, (void *)line.Address, line.FileName, line.LineNumber);
            result.push_back(buf);
        }
    }
 
    free(backtrace);
    free(sym);
 
    return result;
}
 
int main()
{
    auto r = capture_stack_backtrace(64);
    for (int i = 0; i < r.size(); i++) {
        std::cout << r[i] << std::endl;
    }
}
      

获取调用堆栈的函数 CaptureStackBackTrace定义如下:

USHORT WINAPI CaptureStackBackTrace(
  _In_      ULONG  FramesToSkip,
  _In_      ULONG  FramesToCapture,
  _Out_     PVOID  *BackTrace,
  _Out_opt_ PULONG BackTraceHash
);
  • 程序输出
0: at capture_stack_backtrace (0x00C28E6D)
        in d:\source\consoleapplication19\consoleapplication19\consoleapplication19.cpp(18)
1: at main (0x00C29DCA)
        in d:\source\consoleapplication19\consoleapplication19\consoleapplication19.cpp(52)
2: at invoke_main (0x00C2A783)
        in d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(78)
3: at __scrt_common_main_seh (0x00C2A602)
        in d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
4: at __scrt_common_main (0x00C2A49D)
        in d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(331)
5: at mainCRTStartup (0x00C2A818)
        in d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp(17)
上面的例子所打印的函数调用堆栈就是和我们在高级语言中看到的异常堆栈是一样的,为了能够捕捉异常,我们还需要一些工作。
  • 重定义信号捕捉
class Signals
{
public:
    typedef enum {
        Interrupt = 0x01,
        IllegalInstruction = 0x02,
        FloatingPointException = 0x04,
        SegmentViolation = 0x08,
        SoftwareTermination = 0x10,
        CtrlBreak = 0x20,
        AbortTermination = 0x40,
        All = 0x7F
    } signal_t;

    typedef void(*signal_handler_t)(signal_t);

public:
    static void install();
    static void uninstall();
    static void attach(signal_t sig, signal_handler_t handler);
    static void debug(signal_t sig);
    static void raise(signal_t sig);
};
      
struct signal_entry_t
{
    Signals::signal_t sig;
    int signum;
    const char *name;
    Signals::signal_handler_t handler;
    _crt_signal_t origin_function;
    signal_entry_t *next_entry;

    signal_entry_t(Signals::signal_t sig, int signum, const char *name, signal_entry_t * next_entry = nullptr)
    {
        this->sig = sig;
        this->signum = signum;
        this->name = name;
        this->handler = nullptr;
        this->origin_function = nullptr;
        this->next_entry = next_entry;
    }
};

signal_entry_t * build_signal_entries()
{
    signal_entry_t *entries =
        new signal_entry_t(Signals::Interrupt, SIGINT, "Interrupt",
        new signal_entry_t(Signals::IllegalInstruction, SIGILL, "IllegalInstruction",
        new signal_entry_t(Signals::SegmentViolation, SIGSEGV, "SegmentViolation",
        new signal_entry_t(Signals::SegmentViolation, SIGSEGV, "SegmentViolation",
        new signal_entry_t(Signals::SegmentViolation, SIGSEGV, "SegmentViolation",
        new signal_entry_t(Signals::SoftwareTermination, SIGTERM, "SoftwareTermination",
        new signal_entry_t(Signals::CtrlBreak, SIGBREAK, "CtrlBreak",
        new signal_entry_t(Signals::AbortTermination, SIGABRT, "AbortTermination"))))))));
    return entries;
}

signal_entry_t * _signal_entries = build_signal_entries();

void _signal_default_handler(int signum)
{
    auto e = _signal_entries;
    while (e != nullptr) {
        if (e->signum == signum) {
            if (e->handler != nullptr) {
                e->handler(e->sig);
            }
            else {
                e->origin_function(e->signum);
            }
            break;
        }
        e = e->next_entry;
    }
}

void dbg_signal_handler(Signals::signal_t sig)
{
    auto e = _signal_entries;
    while (e != nullptr) {
        if (e->sig == sig) {
            auto backtrace = Debug::capture_stack_backtrace(64, 3);
            Debug::kprintf("Signal Handled: %s\n%s", e->name, backtrace.c_str());
            break;
        }
        e = e->next_entry;
    }
}

void Signals::install()
{
    auto e = _signal_entries;
    while (e != nullptr) {
        e->origin_function = signal(e->signum, _signal_default_handler);
        e = e->next_entry;
    }
}

void Signals::uninstall()
{
    auto e = _signal_entries;
    while (e != nullptr) {
        signal(e->signum, e->origin_function);
        e = e->next_entry;
    }
}

void Signals::attach(signal_t sig, signal_handler_t handler)
{
    auto e = _signal_entries;
    while (e != nullptr) {
        if (e->sig & sig) {
            e->handler = handler;
        }
        e = e->next_entry;
    }
}

void Signals::debug(signal_t sig)
{
    attach(sig, dbg_signal_handler);
}

void Signals::raise(signal_t sig)
{
    auto e = _signal_entries;
    while (e != nullptr) {
        if (e->sig & sig) {
            ::raise(e->signum);
        }
        e = e->next_entry;
    }
}
      
  • 测试信号捕捉
void test_raise_abrt() {
    se_exception::install();
    Signals::install();
    Signals::debug(Signals::All);
    Debug::puts("Now raise SIGABRT...");
    ::raise(SIGABRT);
    Debug::puts("Program End.");
}
      
  • 测试结果,如期捕获
Now raise SIGABRT...
Signal Handled: AbortTermination
at test_raise_abrt in d:\home\source\klibc\test\test.cpp(30)
at main in d:\home\source\klibc\test\test.cpp(129)
at invoke_main in d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(78)
at __scrt_common_main_seh in d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
at __scrt_common_main in d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(331)
at mainCRTStartup in d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp(17)
Program End.
  • 异常的定义和转换
class se_exception
{
private:
    Array<void *> _frames;
    int _se_code;
    void * _ex_pointers;
    static _se_translator_function _original_translator;

public:
    se_exception(unsigned int se_code, void * ex_pointers, Array<void *> frames);
    se_exception(const se_exception & other);
    int se_code() const;
    void * exception_pointers() const;
    String message() const;
    String backtrace() const;

public:
    static void install();
    static void uninstall();
};
      
String _se_get_error_message(unsigned int se_code, const void * addr)
{
    auto fmtmsg = [&](auto msg) {
        return String::format("Exception %s (0x%.8x) at address 0x%p.", msg, se_code, addr);
    };

    switch (se_code) {
    case EXCEPTION_ACCESS_VIOLATION:
        return fmtmsg("EXCEPTION_ACCESS_VIOLATION");
    case EXCEPTION_DATATYPE_MISALIGNMENT:
        return fmtmsg("EXCEPTION_DATATYPE_MISALIGNMENT");
    case EXCEPTION_BREAKPOINT:
        return fmtmsg("EXCEPTION_BREAKPOINT");
    case EXCEPTION_SINGLE_STEP:
        return fmtmsg("EXCEPTION_SINGLE_STEP");
    case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
        return fmtmsg("EXCEPTION_ARRAY_BOUNDS_EXCEEDED");
    case EXCEPTION_FLT_DENORMAL_OPERAND:
        return fmtmsg("EXCEPTION_FLT_DENORMAL_OPERAND");
    case EXCEPTION_FLT_DIVIDE_BY_ZERO:
        return fmtmsg("EXCEPTION_FLT_DIVIDE_BY_ZERO");
    case EXCEPTION_FLT_INEXACT_RESULT:
        return fmtmsg("EXCEPTION_FLT_INEXACT_RESULT");
    case EXCEPTION_FLT_INVALID_OPERATION:
        return fmtmsg("EXCEPTION_FLT_INVALID_OPERATION");
    case EXCEPTION_FLT_OVERFLOW:
        return fmtmsg("EXCEPTION_FLT_OVERFLOW");
    case EXCEPTION_FLT_STACK_CHECK:
        return fmtmsg("EXCEPTION_FLT_STACK_CHECK");
    case EXCEPTION_FLT_UNDERFLOW:
        return fmtmsg("EXCEPTION_FLT_UNDERFLOW");
    case EXCEPTION_INT_DIVIDE_BY_ZERO:
        return fmtmsg("EXCEPTION_INT_DIVIDE_BY_ZERO");
    case EXCEPTION_INT_OVERFLOW:
        return fmtmsg("EXCEPTION_INT_OVERFLOW");
    case EXCEPTION_PRIV_INSTRUCTION:
        return fmtmsg("EXCEPTION_PRIV_INSTRUCTION");
    case EXCEPTION_IN_PAGE_ERROR:
        return fmtmsg("EXCEPTION_IN_PAGE_ERROR");
    case EXCEPTION_ILLEGAL_INSTRUCTION:
        return fmtmsg("EXCEPTION_ILLEGAL_INSTRUCTION");
    case EXCEPTION_NONCONTINUABLE_EXCEPTION:
        return fmtmsg("EXCEPTION_NONCONTINUABLE_EXCEPTION");
    case EXCEPTION_STACK_OVERFLOW:
        return fmtmsg("EXCEPTION_STACK_OVERFLOW");
    case EXCEPTION_INVALID_DISPOSITION:
        return fmtmsg("EXCEPTION_INVALID_DISPOSITION");
    case EXCEPTION_GUARD_PAGE:
        return fmtmsg("EXCEPTION_GUARD_PAGE");
    case EXCEPTION_INVALID_HANDLE:
        return fmtmsg("EXCEPTION_INVALID_HANDLE");
    default:
        return fmtmsg("UNKNOWN_ERROR");
    }
}

void _se_translator(UINT se_code, _EXCEPTION_POINTERS* ex_pointers)
{
    Array<void *> frames(128);
    ::CaptureStackBackTrace(0, 128, frames.ptr(), NULL);
    frames[0] = ex_pointers->ExceptionRecord->ExceptionAddress;
    throw se_exception(se_code, ex_pointers, frames);
}

se_exception::se_exception(unsigned int se_code, void * ex_pointers, Array<void *> frames)
{
    _se_code = se_code;
    _ex_pointers = ex_pointers;
    _frames = frames;
}

se_exception::se_exception(const se_exception & other)
{
    _se_code = other._se_code;
    _ex_pointers = other._ex_pointers;
    _frames = other._frames;
}

int se_exception::se_code() const
{
    return _se_code;
}

void * se_exception::exception_pointers() const
{
    return _ex_pointers;
}

String se_exception::message() const
{
    auto addr = reinterpret_cast<_EXCEPTION_POINTERS *>(_ex_pointers)->ExceptionRecord->ExceptionAddress;
    return _se_get_error_message(_se_code, addr);
}

String se_exception::backtrace() const
{
    return Debug::get_backtrace((void **)_frames.ptr(), _frames.size());
}

void se_exception::install()
{
    _original_translator = _set_se_translator(_se_translator);
}

void se_exception::uninstall()
{
    _set_se_translator(_original_translator);
}

_se_translator_function se_exception::_original_translator;
      
  • 测试异常捕捉
void test_se() {
    try {
        se_exception::install();
        Signals::install();
        Signals::debug(Signals::All);
        Debug::puts("Now call div_by_zero...");
        div_by_zero();
        Debug::puts("Program End.");
    }
    catch (se_exception &ex) {
        Debug::puts("Exception is catched:");
        Debug::puts(ex.message());
        Debug::puts(ex.backtrace());
    }
}
      
  • 测试结果
Now call div_by_zero...
Exception is catched:
Exception EXCEPTION_INT_DIVIDE_BY_ZERO (0xc0000094) at address 0x00555DB5.
at div_by_zero in d:\home\source\klibc\test\test.cpp(21)
at test_se in d:\home\source\klibc\test\test.cpp(40)
at main in d:\home\source\klibc\test\test.cpp(122)
at invoke_main in d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(78)
at __scrt_common_main_seh in d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
at __scrt_common_main in d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(331)
at mainCRTStartup in d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp(17)
值得注意的是 SAFESEH 只适用于 x86 平台,并不适用于 x64 和 ARM 平台,请参考 MSDN 如下说明:

/SAFESEH is only valid when linking for x86 targets. /SAFESEH is not supported for platforms that already have the exception handlers noted. For example, on x64 and ARM, all exception handlers are noted in the PDATA. ML64.exe has support for adding annotations that emit SEH information (XDATA and PDATA) into the image, allowing you to unwind through ml64 functions. See MASM for x64 (ml64.exe) for more information.