文章目录
Qt生成日志与以及报错文件(mingw64位,winDbg)
0 背景与结果
0.1 背景
因为编写的Qt软件,编译成运用程序发布后,会遇到莫名的崩溃的问题。为了找到问题所在,解决的方法就是记录日志和捕获崩溃错误的信息。
因为作者使用mingw64位的编译器,所以这里使用ccrashstack类
(专为Qt-Mingw32环境设计的工具类),生成.dmp 文件
,来帮助开发者调试程序中的bug。
除了使用ccrashstack类
,也可以使用Google breakpad
,qBreakpad
(适用MSVC编译器(而mingw编译器,不支持生成Microsoft PDB格式)。需要编译源代码,会稍微麻烦一些,点击可以查看操作方法)。
0.2 结果
使用项目生成dmp文件,
使用winDbg打开文件,调试获得的结果如下:
可以看出结果非常nice,可以知道错误类型 NULL_POINTER_READ_NULL_INSTRUCTION_PTR_INVALID_POINTER_READ
,错误的行数在哪里。非常适合测试发布版本,出现的异常闪退的问题。
生成的日志信息也非常方便,程序运行的状况。
1 WinDbg
1.1 安装
捕获生成的dmp异常文件,需要使用WinDbg
来进行解析。因此需要先下载和安装WinDbg。
若要使用 Windows 包管理器安装 WinDbg,请从命令行/PowerShell
运行以下命令:
winget install Microsoft.WinDbg
安装完成后,可以在搜索中找到安装好的调试软件。
1.2 使用
1,设置源代码路径(如果源代码修改太多,定位的时候可以会出错);
如果存在符号路径,也可以填写,不填写的时候,会联网下载;
2,打开dmp文件;
如果是发布的程序,则需要把exe、所有库都放在一起,如下图所示:
3,按照图片上的先点击.ecxr
,再点击!analyze -v
,就可以得到最开头的结果;
4,如果熟悉WinDbg的,可以在这个框中,输入调试指令;
2 编写代码
2.1 ccrashstack类
因为编译的环境为mingw64位,因此需要对32位的CCrashStack
类进行修改,修改的地方如下:
1,按照提示把32位的Eax、Ebx、Exc、Edx、Esi、Edi、Esp、Ebp、Eip的首字母改成R开头的64位的接口;
2, error: cast from 'PVOID' {aka 'void*'} to 'int' loses precision [-fpermissive] sprintf(buffer, "Exception Addr: %08X ", (int)E.ExceptionAddress);
。根据报错信息得知,void*
类型转int会遗失精度(在64位系统中,内存使用64位地址,所以指针的大小也是64位。 void* 指针是64位的,而 int 通常是32位的,因此转换时可能会丢失高位信息。),使用reinterpret_cast
把void*
指针转换为 uintptr_t
类型,这是一个无符号整数类型(在 64 位机器上,uintptr_t
被定义为 unsigned long int
),其大小与指针相同,然后使用static_cast
再将 uintptr_t
类型的整数转换为int
类型;
3, error: cast from 'PBYTE' {aka 'unsigned char*'} to 'unsigned int' loses precision
。根据报错信息得知,unsigned char*
转unsigned int
会损失精度。错误原因同上,64位的指针地址,转为32位的无符号整型,会遗失精度报错。解决方法为把unsigned char*
的Ebp->Ret_Addr
转换为unsigned long long
类型,并使用 %016llX
来确保能完整输出64位十六进制值;
这里只展示头文件,具体文件见附录:
#ifndef CCRASHSTACK_H
#define CCRASHSTACK_H
#include <windows.h>
#include <QString>
class CCrashStack
{
private:
PEXCEPTION_POINTERS m_pException;
private:
QString GetModuleByRetAddr(PBYTE Ret_Addr, PBYTE & Module_Addr);
QString GetCallStack(PEXCEPTION_POINTERS pException);
QString GetVersionStr();
bool GetHardwareInaformation(QString &graphics_card, QString &sound_deivce);
public:
CCrashStack(PEXCEPTION_POINTERS pException);
QString GetExceptionInfo();
};
#endif // CCRASHSTACK_H
2.2 编写输出捕获异常的dmp文件
SetErrorMode
函数的用法如下:
#ifndef OUTPUTDUMP_H
#define OUTPUTDUMP_H
//outputDump.h
//#pragma once //避免同一个文件只会被包含一次
#include "ccrashstack.h"
#include <shlobj.h>
#include <QDebug>
#include <DbgHelp.h>
#include <QDateTime>
//#pragma comment(lib, "dbghelp.lib") //使用注释方式引入库dbghelp.lib或编译目录。
/*------------------生成dump文件------------------------*/
LONG crashHandler(EXCEPTION_POINTERS *pException)
{
qDebug()<<"LONG crashHandler(EXCEPTION_POINTERS *pException)";
QString curDataTime = QDateTime::currentDateTime().toString("yyyyMMddhhmmss");
QString dumpName = curDataTime + ".dmp";
HANDLE dumpFile = CreateFile((LPCWSTR)QString("./" + dumpName).utf16(),GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(dumpFile != INVALID_HANDLE_VALUE)
{
MINIDUMP_EXCEPTION_INFORMATION dumpInfo;
dumpInfo.ExceptionPointers = pException;
dumpInfo.ThreadId = GetCurrentThreadId();
dumpInfo.ClientPointers = TRUE;
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),dumpFile, MiniDumpNormal, &dumpInfo, NULL, NULL);
CloseHandle(dumpFile);
}
else
{
qDebug() << "dumpFile not vaild";
}
return EXCEPTION_EXECUTE_HANDLER;
}
//防止CRT(C runtime)函数报错可能捕捉不到(64位会报错)
void DisableSetUnhandledExceptionFilter()
{
//原理:https://learn.microsoft.com/zh-cn/windows/win32/api/errhandlingapi/nf-errhandlingapi-seterrormode
SetErrorMode(SetErrorMode(0) | SEM_NOGPFAULTERRORBOX);
/*
在64位系统中运行该函数闪退,可能是由于代码尝试修改系统函数 SetUnhandledExceptionFilter 的内存,而64位系统有更严格的内存保护机制,导致内存写入失败。
*/
// void* addr = (void*)GetProcAddress(LoadLibrary(L"kernel32.dll"), "SetUnhandledExceptionFilter");
// if(addr)
// {
// unsigned char code[16];
// int size = 0;
// code[size++] = 0x33;
// code[size++] = 0xC0;
// code[size++] = 0xC2;
// code[size++] = 0x04;
// code[size++] = 0x00;
// DWORD dwOldFlag, dwTempFlag;
// VirtualProtect(addr, size, PAGE_READWRITE, &dwOldFlag);
// WriteProcessMemory(GetCurrentProcess(), addr, code, size, NULL);
// VirtualProtect(addr, size, dwOldFlag, &dwTempFlag);
// }
}
#endif // OUTPUTDUMP_H
2.2 编写输出日志文件
把Qt5个类型的日志消息,附加上产生时间、文件名、行数、函数名称的信息,写入到txt
文件中。控制日志的大小,每当写入5000次时,检查日志大小是否超过15M,如果超过,则清空重新写入。
对于日志内容,放入到QQueue
中,进行异步上锁操作。
#ifndef OUTPUT_LOG_H
#define OUTPUT_LOG_H
//outputLog.h
//#pragma once //避免同一个文件不会被包含多次
#include <QDir>
#include <QFile>
#include <QMutex>
#include <QTextStream>
#include <QTime>
// #include"ccrashstack.h"
#include <QQueue>
#include <QFileInfo>
#include <fstream>
std::ofstream g_OutputDebug;
// 使用异步日志队列
QQueue<QString> logQueue;
QMutex queueMutex;
void enqueueLogMessage(const QString &message) {
QMutexLocker locker(&queueMutex);
logQueue.enqueue(message);
// 可以在此处触发异步写入操作
}
/*---------------打日志文件----------------------*/
void outputMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
// static QMutex mutex;
// mutex.lock();
enqueueLogMessage(msg); // 将消息加入队列,而非直接写入
static int count = 0;
QString text;
switch(type)
{
case QtDebugMsg:
text = QString("[Debug]");
break;
case QtWarningMsg:
text = QString("[Warning]");
break;
case QtCriticalMsg:
text = QString("[Critical]");
break;
case QtInfoMsg:
text = QString("[Information]");
break;
break;
case QtFatalMsg:
text = QString("[Fatal]");
}
// 获取纯文件名
QFileInfo fileInfo(context.file);
QString filename = fileInfo.baseName();
// QString context_info = QString("File:(%1) Line:(%2) Function:(%3)").arg(filename).arg(context.line).arg(context.function);
QString context_info = QString("( %1:%2, %3 )").arg(filename).arg(context.line).arg(context.function);
QString current_date_time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss ddd");
QString current_date = QString("[%1]").arg(current_date_time);
QString message = QString("%1 %2 %3 \r\n%4").arg(current_date).arg(text).arg(context_info).arg(msg);
//判断文件夹是否存在,不存在新建
QString aFile = QDir::currentPath() + "/LogFile";
QDir dir(aFile);
if(!dir.exists())
{
dir.mkdir(aFile);//只创建一级子目录,即必须保证上级目录存在
}
QString current_time = QDateTime::currentDateTime().toString("yyyyMMdd");
QFile file(aFile+"/log"+current_time+".txt");
file.open(QIODevice::WriteOnly | QIODevice::Append);
QTextStream text_stream(&file);
text_stream << message << "\r\n \r\n";
file.flush();
file.close();
// mutex.unlock();
count ++;
//每打印5000次判断一次文件大小,超过5M就清空重新写入
QFileInfo info(aFile+"/log"+current_time+".txt");
if(count > 5000)
{
count = 0;
if(info.size() > 1024*1024*15)//1024*1024*5
{
g_OutputDebug.open(qPrintable(aFile+"/log"+current_time+".txt"), std::ios::out | std::ios::trunc);
g_OutputDebug.close();
}
}
}
#endif // OUTPUT_LOG_H
2.3 调用生成日志和dmp文件
调用Windows的系统SetUnhandledExceptionFilter
函数(该函数有个设置回调函数,软件崩溃时会回调该系统函数,并传回崩溃地址信息等,),给该函数传递编写的crashHandler
函数中,此函数的MiniDumpWriteDump
方法(MiniDumpWriteDump
是一个 Windows API 函数,用于将用户模式小型转储信息写入指定的文件。这个函数在调试和分析程序崩溃时非常有用,可以帮助开发人员收集异常信息并进行故障排除)来把产生的错误内容写入到.dmp
文件中。
pro
文件中加入如下配置:
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_MESSAGELOGCONTEXT
DEFINES += QT_DEPRECATED_WARNINGS
# # 添加DUMP文件
QMAKE_CXXFLAGS_RELEASE = $$QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO
QMAKE_LFLAGS_RELEASE = $$QMAKE_LFLAGS_RELEASE_WITH_DEBUGINFO,
QMAKE_LFLAGS_RELEASE = /INCREMENTAL:NO /DEBUG
# test crash
QMAKE_CFLAGS_RELEASE += -g
QMAKE_CXXFLAGS_RELEASE += -g
QMAKE_CFLAGS_RELEASE -= -O2
QMAKE_CXXFLAGS_RELEASE -= -O2
QMAKE_LFLAGS_RELEASE = -mthreads -W
# 方便生成DUMP调试
LIBS += -lDbgHelp
QMAKE_LFLAGS_RELEASE = /INCREMENTAL:NO /DEBUG
QMAKE_CXXFLAGS += -g
QMAKE_CFLAGS += -g
# 调试信息以及pdb文件
QMAKE_CXXFLAGS_RELEASE = $$QMAKE_CXXFLAGS_RELEASE_WITH_DEBUGINFO
QMAKE_CFLAGS_RELEASE = $$QMAKE_CLAGS_RELEASE_WITH_DEBUGINFO
QMAKE_LFLAGS_RELEASE = $$QMAKE_LFLAGS_RELEASE_WITH_DEBUGINFO
LIBS += -lpsapi
调用方法如下:
#include"outputDump.h" //生成dump头文件
#include"outputLog.h" //生成日志头文件
int main(int argc, char *argv[])
{
// // /*-------1、注冊异常捕获函数 生成.dmp文件--------*/
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)crashHandler);
DisableSetUnhandledExceptionFilter();
QApplication a(argc, argv);
// // /*-------2、注册MessageHandler 生成日志文件--------*/
qInstallMessageHandler(outputMessage);
MainInterface w;
w.show();
return a.exec();
}
参考
# Qt环境生成dump文件解决程序异常崩溃以及生成日志文件