简介

在开发中我们一定要避免崩溃出现,不过在release发布后的程序中的崩溃也不是完全能掌握。为了能够全面的找到问题所在,输出崩溃时的信息有助于我们排查问题。

下面的代码能提供崩溃时记录错误内容代码行号堆栈等关键信息

主要利用SetUnhandledExceptionFilter设置异常捕获回调,在回调中处理消息输出。

SetUnhandledExceptionFilter 是 Windows API 中的一个函数,用于设置全局的未捕获异常处理程序。当应用程序中发生未捕获的异常时,此函数指定的处理程序将被调用。

输出样式

测试代码:

int* p = NULL;
    *p = 1;

结果:path->user\AppData\Local\"appname"\log\crash_20240825_212443.log

C++崩溃日志输出(Windows Qt)_日志

内容:

可以看到输出了具体错误与运行堆栈,与代码文件+行号

Access violation at address 0x7ff7564390a0 (code 0xc0000005)
0x7ff7564390a0: main at D:\Code\tscore2\src\main.cpp:125
0x7ff7565f6ed9: invoke_main at D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:79
0x7ff7565f6dbe: __scrt_common_main_seh at D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
0x7ff7565f6c7e: __scrt_common_main at D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:331
0x7ff7565f6f6e: mainCRTStartup at D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp:17
0x7ffaaa7f257d: BaseThreadInitThunk
0x7ffaab66aa58: RtlUserThreadStart

案例代码

//头文件
//Win
#include <windows.h>
#include <dbghelp.h>
//C++
#include <iostream>
//QT
#include <QDir>
#include <QDateTime>
#include <QSysInfo>
#include <QStandardPaths>
#include <QTextStream>
#include <QFile>
#ifdef Q_OS_WIN32
#pragma comment(lib, "dbghelp.lib")

 LONG WINAPI unhandledExceptionHandler(EXCEPTION_POINTERS* exceptionInfo)
{

    std::cerr << "Unhandled exception handler called" << std::endl;
    // 打印异常代码
    QString crashLog;
    switch (exceptionInfo->ExceptionRecord->ExceptionCode) {
    case EXCEPTION_ACCESS_VIOLATION:
        crashLog += QString("Access violation at address 0x%1 (code 0x%2)\n")
                  .arg((quintptr)exceptionInfo->ExceptionRecord->ExceptionAddress, 0, 16)
                  .arg(exceptionInfo->ExceptionRecord->ExceptionCode, 0, 16);
        break;

    case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
        crashLog += "Array bounds exceeded\n";
        break;

    case EXCEPTION_FLT_DENORMAL_OPERAND:
        crashLog += "Denormal operand\n";
        break;

    case EXCEPTION_FLT_DIVIDE_BY_ZERO:
        crashLog += "Floating point divide by zero\n";
        break;

    case EXCEPTION_FLT_INEXACT_RESULT:
        crashLog += "Inexact floating point result\n";
        break;

    case EXCEPTION_FLT_INVALID_OPERATION:
        crashLog += "Invalid floating point operation\n";
        break;

    case EXCEPTION_FLT_OVERFLOW:
        crashLog += "Floating point overflow\n";
        break;

    case EXCEPTION_FLT_STACK_CHECK:
        crashLog += "Floating point stack check\n";
        break;

    case EXCEPTION_FLT_UNDERFLOW:
        crashLog += "Floating point underflow\n";
        break;

    case EXCEPTION_INT_DIVIDE_BY_ZERO:
        crashLog += "Integer divide by zero\n";
        break;

    case EXCEPTION_INT_OVERFLOW:
        crashLog += "Integer overflow\n";
        break;
    default:
        crashLog += QString("Unknown exception at address 0x%1 (code 0x%2)")
                        .arg((quintptr)exceptionInfo->ExceptionRecord->ExceptionAddress, 0, 16)
                        .arg(exceptionInfo->ExceptionRecord->ExceptionCode, 0, 16);
        break;
    }

    // 获取堆栈跟踪
    HANDLE process = GetCurrentProcess();
    HANDLE thread = GetCurrentThread();
    CONTEXT context = *exceptionInfo->ContextRecord;

    SymInitialize(process, NULL, TRUE);

    STACKFRAME64 stackFrame;
    memset(&stackFrame, 0, sizeof(STACKFRAME64));

#ifdef _M_IX86
    DWORD machineType = IMAGE_FILE_MACHINE_I386;
    stackFrame.AddrPC.Offset = context.Eip;
    stackFrame.AddrPC.Mode = AddrModeFlat;
    stackFrame.AddrFrame.Offset = context.Ebp;
    stackFrame.AddrFrame.Mode = AddrModeFlat;
    stackFrame.AddrStack.Offset = context.Esp;
    stackFrame.AddrStack.Mode = AddrModeFlat;
#elif _M_X64
    DWORD machineType = IMAGE_FILE_MACHINE_AMD64;
    stackFrame.AddrPC.Offset = context.Rip;
    stackFrame.AddrPC.Mode = AddrModeFlat;
    stackFrame.AddrFrame.Offset = context.Rsp;
    stackFrame.AddrFrame.Mode = AddrModeFlat;
    stackFrame.AddrStack.Offset = context.Rsp;
    stackFrame.AddrStack.Mode = AddrModeFlat;
#elif _M_IA64
    DWORD machineType = IMAGE_FILE_MACHINE_IA64;
    stackFrame.AddrPC.Offset = context.StIIP;
    stackFrame.AddrPC.Mode = AddrModeFlat;
    stackFrame.AddrFrame.Offset = context.IntSp;
    stackFrame.AddrFrame.Mode = AddrModeFlat;
    stackFrame.AddrBStore.Offset = context.RsBSP;
    stackFrame.AddrBStore.Mode = AddrModeFlat;
    stackFrame.AddrStack.Offset = context.IntSp;
    stackFrame.AddrStack.Mode = AddrModeFlat;
#endif

    while (StackWalk64(machineType, process, thread, &stackFrame, &context, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL))
    {
        DWORD64 address = stackFrame.AddrPC.Offset;
        if (address == 0)
        {
            break;
        }

        // 获取符号信息
        DWORD64 displacement = 0;
        char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
        PSYMBOL_INFO symbol = (PSYMBOL_INFO)buffer;
        symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
        symbol->MaxNameLen = MAX_SYM_NAME;

        if (SymFromAddr(process, address, &displacement, symbol))
        {
            crashLog += QString("0x%1: %2").arg(address, 0, 16).arg(symbol->Name);

            // 获取文件名和行号
            IMAGEHLP_LINE64 line;
            DWORD lineDisplacement;
            memset(&line, 0, sizeof(line));
            line.SizeOfStruct = sizeof(line);

            if (SymGetLineFromAddr64(process, address, &lineDisplacement, &line))
            {
                crashLog += QString(" at %1:%2\n").arg(line.FileName).arg(line.LineNumber);
            }
            else
            {
                crashLog += "\n";
            }
        }
        else
        {
            crashLog += QString("0x%1: [unknown function]\n").arg(address, 0, 16);
        }
    }

    SymCleanup(process);

    // 创建新的日志文件
    QString logDirPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/log";
    QDir logDir(logDirPath);
    if (!logDir.exists()) {
        logDir.mkpath(logDirPath);
    }
    QString logFilePath = logDir.filePath(QString("crash_%1.log").arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss")));
    QFile logFile(logFilePath);
    if (!logFile.open(QFile::WriteOnly | QFile::Text)) {
        std::cerr << "Can't open file to write: " << qPrintable(logFile.errorString()) << std::endl;
        return EXCEPTION_EXECUTE_HANDLER;
    }
    QTextStream logStream(&logFile);
    logStream << crashLog << endl;
    logStream.flush();
    logFile.flush();
    logFile.close();
    std::cerr << "Crash log written to file:: " << qPrintable(logFilePath) << std::endl;
    // 返回值表示是否继续执行
    // 如果返回值为 EXCEPTION_CONTINUE_SEARCH,则继续查找异常处理程序
    // 如果返回值为 EXCEPTION_CONTINUE_EXECUTION,则忽略异常并继续执行
    // 如果返回值为 EXCEPTION_EXECUTE_HANDLER,则执行当前异常处理程序
    return EXCEPTION_EXECUTE_HANDLER;
}
#endif

注意在使用log之前运行下面这段话

//记得在其他地方注册
// 注册未处理异常处理函数
SetUnhandledExceptionFilter(unhandledExceptionHandler);

//example
extern LONG WINAPI unhandledExceptionHandler(EXCEPTION_POINTERS* exceptionInfo);
int main(int argc, char *argv[])
{
    SetUnhandledExceptionFilter(unhandledExceptionHandler);
}