跨平台崩溃捕获库 - 纯C++实现,支持Windows/Mac/Linux

跨平台崩溃捕获库 - 纯C++实现,支持Windows/Mac/Linux

简介

这是一个轻量级的跨平台崩溃捕获库,可以自动捕获程序崩溃并生成包含堆栈信息的转储文件。代码简洁,无第三方依赖,打开即可编译运行。

核心特性:

  • 纯C++17实现,无第三方依赖
  • Windows平台生成 .dmp 文件(支持WinDbg分析)
  • Linux/Mac平台生成 .txt 文件(包含符号化的堆栈跟踪)
  • 支持崩溃回调通知
  • 线程安全

源码

CrashDump.h

#ifndef CRASHDUMP_H_
#define CRASHDUMP_H_

#include <string>
#include <functional>

#ifndef CRASHDUMP_EXPORT
    #ifdef _MSC_VER
    #ifdef BUILD_CRASHDUMP
        #define CRASHDUMP_EXPORT __declspec(dllexport)
    #else
        #define CRASHDUMP_EXPORT __declspec(dllimport)
    #endif
    #elif defined(__GNUC__)
        #define CRASHDUMP_EXPORT __attribute__((visibility("default")))
    #endif
#endif

typedef std::function<int(const std::string&)> DumpCallback;

void CRASHDUMP_EXPORT InitCrashDump(const std::string& dumpFilePath, DumpCallback cb = {});

#endif

CrashDump.cpp

#include "CrashDump.h"

#include <csignal>
#include <csetjmp>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>

#include <string>
#include <atomic>
#include <mutex>
#include <fstream>

#ifdef _WIN32
    #define WIN32_LEAN_AND_MEAN
    #include <windows.h>
    #include <dbghelp.h>
    #pragma comment(lib, "dbghelp.lib")
#else
    #include <unistd.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <execinfo.h>
    #include <fcntl.h>
    #include <ucontext.h>
    #include <cxxabi.h>
    #include <dlfcn.h>
#endif

namespace {

std::string g_dumpPath;
DumpCallback g_callback;
std::atomic<bool> g_crashed{false};
std::mutex g_mutex;

#ifdef _WIN32
BOOL CALLBACK MiniDumpCallback(
    PVOID callbackParam,
    PMINIDUMP_CALLBACK_INPUT callbackInput,
    PMINIDUMP_CALLBACK_OUTPUT callbackOutput
) {
    (void)callbackParam;
    (void)callbackOutput;

    if (callbackInput->CallbackType == ModuleCallback ||
        callbackInput->CallbackType == IncludeModuleCallback ||
        callbackInput->CallbackType == IncludeThreadCallback ||
        callbackInput->CallbackType == ThreadCallback ||
        callbackInput->CallbackType == ThreadExCallback) {
        return TRUE;
    }
    return FALSE;
}

LONG WINAPI ExceptionHandler(LPEXCEPTION_POINTERS exceptionInfo) {
    if (g_crashed.exchange(true)) {
        return EXCEPTION_CONTINUE_SEARCH;
    }

    std::lock_guard<std::mutex> lock(g_mutex);

    std::string filePath = g_dumpPath;
    if (filePath.empty()) {
        char tempPath[MAX_PATH];
        GetTempPathA(MAX_PATH, tempPath);
        filePath = std::string(tempPath) + "crash.dmp";
    }

    HANDLE hFile = CreateFileA(
        filePath.c_str(),
        GENERIC_WRITE,
        FILE_SHARE_WRITE | FILE_SHARE_READ,
        nullptr,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        nullptr
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        return EXCEPTION_CONTINUE_SEARCH;
    }

    MINIDUMP_EXCEPTION_INFORMATION exceptInfo;
    exceptInfo.ThreadId = GetCurrentThreadId();
    exceptInfo.ExceptionPointers = exceptionInfo;
    exceptInfo.ClientPointers = FALSE;

    MINIDUMP_TYPE dumpType = MiniDumpNormal
        | MiniDumpWithIndirectlyReferencedMemory
        | MiniDumpWithDataSegs
        | MiniDumpWithHandleData
        | MiniDumpWithFullMemoryInfo
        | MiniDumpWithThreadInfo
        | MiniDumpWithUnloadedModules;

    MINIDUMP_CALLBACK_INFORMATION callbackInfo;
    callbackInfo.CallbackRoutine = MiniDumpCallback;
    callbackInfo.CallbackParam = nullptr;

    BOOL success = MiniDumpWriteDump(
        GetCurrentProcess(),
        GetCurrentProcessId(),
        hFile,
        dumpType,
        &exceptInfo,
        nullptr,
        &callbackInfo
    );

    CloseHandle(hFile);

    if (success && g_callback) {
        g_callback(filePath);
    }

    return EXCEPTION_EXECUTE_HANDLER;
}
#else
struct sigaction g_oldSigHandlers[64];
sigjmp_buf g_sigJumpBuffer;

void PrintStackTrace(std::ofstream& out, int skipFrames) {
    void* buffer[128];
    int nframes = backtrace(buffer, 128);
    char** symbols = backtrace_symbols(buffer, nframes);

    out << "\n========== Stack Trace ==========\n";

    for (int i = skipFrames; i < nframes; ++i) {
        out << "Frame " << (i - skipFrames) << ": " << buffer[i] << "\n";

        if (symbols && symbols[i]) {
            Dl_info info;
            if (dladdr(buffer[i], &info) && info.dli_sname) {
                int status = 0;
                char* demangled = __cxxabiv1::__cxa_demangle(
                    info.dli_sname, nullptr, nullptr, &status
                );

                if (status == 0 && demangled) {
                    out << "  Symbol: " << demangled << "\n";
                    free(demangled);
                } else {
                    out << "  Symbol: " << info.dli_sname << "\n";
                }

                if (info.dli_fname) {
                    out << "  Module: " << info.dli_fname << "\n";
                }
                out << "  Offset: " << reinterpret_cast<char*>(buffer[i])
                    << " - " << reinterpret_cast<char*>(info.dli_saddr) << "\n";
            }
        }
    }

    free(symbols);
    out << "================================\n";
}

void SignalHandler(int sig, siginfo_t* info, void* ucontext) {
    sigset_t mask;
    sigfillset(&mask);
    sigdelset(&mask, sig);
    pthread_sigmask(SIG_SETMASK, &mask, nullptr);

    if (sig == SIGABRT) {
        abort();
    }

    std::ofstream out(g_dumpPath, std::ios::app);
    if (out.is_open()) {
        out << "\n===== Signal: " << strsignal(sig) << " (" << sig << ") =====\n";
        if (info) {
            out << "Signal Info: addr=" << info->si_addr
                << " code=" << info->si_code << "\n";
        }
        PrintStackTrace(out, 3);
        out.close();
    }

    if (g_callback) {
        g_callback(g_dumpPath);
    }

    siglongjmp(g_sigJumpBuffer, sig);
}

void InstallSignalHandlers() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sigfillset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = SignalHandler;

    const int signals[] = {
        SIGSEGV, SIGBUS, SIGFPE, SIGABRT, SIGILL, SIGTRAP, SIGSYS
    };

    for (int sig : signals) {
        struct sigaction old;
        if (sigaction(sig, &sa, &old) == 0) {
            g_oldSigHandlers[sig] = old;
        }
    }

    sigsetjmp(g_sigJumpBuffer, 1);
}
#endif

bool EnsureDirectoryExists(const std::string& path) {
    if (path.empty()) return false;

#ifdef _WIN32
    DWORD attrib = GetFileAttributesA(path.c_str());
    if (attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY)) {
        return true;
    }

    size_t pos = path.find_last_of("/\\");
    if (pos != std::string::npos) {
        return EnsureDirectoryExists(path.substr(0, pos));
    }

    return CreateDirectoryA(path.c_str(), nullptr) != 0;
#else
    struct stat st;
    if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
        return true;
    }

    size_t pos = path.find_last_of('/');
    if (pos != std::string::npos) {
        EnsureDirectoryExists(path.substr(0, pos));
    }

    return mkdir(path.c_str(), 0755) == 0 || errno == EEXIST;
#endif
}

std::string GetDefaultDumpPath() {
    std::string path;

#ifdef _WIN32
    char tempPath[MAX_PATH];
    if (GetTempPathA(MAX_PATH, tempPath)) {
        path = tempPath;
    }
#else
    path = "/tmp";
#endif

    time_t now = time(nullptr);
    char timestamp[64];
#ifdef _WIN32
    struct tm tmm;
    localtime_s(&tmm, &now);
    strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", &tmm);
#else
    struct tm* tmm = localtime(&now);
    strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tmm);
#endif

    path += "/crash_";
    path += timestamp;
#ifdef _WIN32
    path += ".dmp";
#else
    path += ".txt";
#endif

    return path;
}

}

void InitCrashDump(const std::string& dumpFilePath, DumpCallback cb) {
    std::lock_guard<std::mutex> lock(g_mutex);

    g_dumpPath = dumpFilePath.empty() ? GetDefaultDumpPath() : dumpFilePath;
    g_callback = cb;

#ifdef _WIN32
    SetUnhandledExceptionFilter(ExceptionHandler);
#else
    if (!dumpFilePath.empty()) {
        size_t lastSep = dumpFilePath.find_last_of("/\\");
        std::string dir = (lastSep != std::string::npos)
            ? dumpFilePath.substr(0, lastSep) : ".";
        EnsureDirectoryExists(dir);
    }
    InstallSignalHandlers();
#endif
}

main.cpp(演示代码)

#include <iostream>
#include "CrashDump.h"

void CauseCrash() {
    int* p = nullptr;
    *p = 42;
}

int main() {
    std::cout << "CrashDump Demo\n";

    InitCrashDump("./crash_dump.txt", [](const std::string& path) {
        std::cout << "\n崩溃已转储: " << path << "\n";
    });

    std::cout << "准备触发崩溃...\n";

#ifdef _WIN32
    Sleep(3000);
#else
    sleep(3);
#endif

    CauseCrash();

    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(CrashDumpDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(CrashDump SHARED CrashDump.h CrashDump.cpp)
target_include_directories(CrashDump PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(CrashDump PRIVATE BUILD_CRASHDUMP)

add_executable(Demo main.cpp)
target_link_libraries(Demo PRIVATE CrashDump)

if(WIN32)
    target_link_libraries(CrashDump PRIVATE dbghelp)
endif()

编译运行

mkdir build && cd build
cmake ..
cmake --build .
./Demo

使用方法

#include "CrashDump.h"

int main() {
    InitCrashDump("./logs/crash.txt", [](const std::string& path) {
        printf("崩溃文件: %s\n", path.c_str());
    });
    
    return 0;
}

实现原理

Windows:通过 SetUnhandledExceptionFilter 注册全局异常处理,崩溃时调用 MiniDumpWriteDump 生成完整的进程快照。

Linux/Mac:通过 sigaction 捕获 SIGSEGVSIGBUS 等信号,使用 backtrace() 获取堆栈,__cxa_demangle 还原C++符号名。

崩溃文件示例

Linux平台生成的 .txt 文件内容:

===== Signal: Segmentation fault (11) =====
Signal Info: addr=0x0 code=128

========== Stack Trace ==========
Frame 0: 0x55a3b4c2a1a7
  Symbol: CauseCrash()
  Module: ./Demo
  Offset: 0x55a3b4c2a1a7 - 0x55a3b4c2a190
Frame 1: 0x55a3b4c2a1f4
  Symbol: main
  Module: ./Demo
  Offset: 0x55a3b4c2a1f4 - 0x55a3b4c2a1c0
================================
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

磊磊cpp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值