洞察探讨小游戏SDK接入的最佳实践以及对企业跨平台开发的优势
1733
2022-09-16
Qt Windows系统使用QBreakpad实战
文章目录
前言1、dump和pdb是什么2、Breakpad介绍3、源码准备
-Breakpad源码-LSS源码-qBreakpad源码
4、编译qBreakpad
将Breakpad、LSS源码放入third_party目录qBreakpad工程介绍参考网上报错,源码bug修正(本人未报错)编译生成qBreakpad.lib
debug 编译release 编译
5、在程序中调用qBreakpad6、生成dump文件7、生成Release pdb文件8、使用VS进行调试
打开dump文件设置pdb文件路径进行调试
9、使用Windbg进行调试
指定pdb文件路径指定代码路径(可选)打开dump文件分析dump文件
10、dump文件上报13、获取崩溃时的通知12、总结
前言
最近想研究下Qt下跨平台的崩溃捕获,经过一番调查,发现有一个来自谷歌的开源项目叫Breakpad,统一了这三平台win、linux、mac生成dump的方式,通过它就可以跨平台。
使用也是相对简单的,大概就是-源码,编译生成lib和dll,然后在你自己的程序中include头文件,就可以在你的程序中集成,在崩溃时生成dump文件。
在我查找Breakpad相关文章时,又发现了一个开源项目叫qBreakpad,这玩意,腻害了,直接将懒癌进行到底,使用Qt对Breakpad进一步封装,使用更简单了。
大致了解了下qBreakpad,该源码简单到无以复加,虽github上文档有些年久失修,但是考虑到如此简单,也就无关痛痒了。
俗话说,站在巨人的肩上看得更远。接下来,我们就选择qBreakpad来生成dump文件吧。
1、dump和pdb是什么
当我们写的程序跑在客户的机器上,因为一个bug,导致程序崩溃,你会有些什么办法来,定位并修复这个bug呢?
有人会说记录日志,即便有日志,也是不好定位的,因为你只能推测出大概的模块或者位置,无法定位到具体出错的代码行。
此时,我们可以让程序崩溃后,自动生成一个*.dmp文件,并配合在编译该程序时生成的pdb文件,来准确定位到调用堆栈、代码行上。这样很轻易就可以找到该bug。
dump文件,后缀 *.dmp,是程序崩溃时的内存转储文件;pdb文件,后缀 *.pdb,是程序的符号文件。
微软有成熟的工具可以分析,比如VS和windbg。所以我们当务之急,就是准备好这2个文件,后面再说如何分析,其实比较简单,耐心就好。
2、Breakpad介绍
当我们写的程序跑在客户的机器上,因为一个bug,导致程序崩溃,你会有些什么办法来,定位并修复这个bug呢?
我们大概先了解下Breakpad的一些常识。
Breakpad是Google公司开发的开源多平台C++崩溃检测库。Breakpad可以捕获发布给用户的应用程序的崩溃,并记录软件崩溃的调试信息到minidump文件中,即*.dmp。
minidump是由微软开发的崩溃记录文件格式。minidump为二进制文件,体积小。为了保持统一,Breakpad在其他系统下也选择生成minidump文件。
除此之外,Breakpad还可以调试信息包括错误行号,报错详情,堆栈错误(stack traces)。支持软件崩溃时候把生成的dump文件上传到自己的服务器上就可以方便的获取崩溃详情。
支持的平台:windows、linux、mac、ios、solaris、android ndk
在不同平台下的实现原理:
Windows:通过SetUnhandledExceptionFilter()设置崩溃回掉函数Max OS:监听 Mach Exception Port 获取崩溃事件Linux:监听 SIGILL SIGSEGV 等异常信号 获取崩溃事件
BreakPad工作原理示意图:
表达的意思就是:
我们在编译的时候,需要在Release版程序中生成调试信息。使用Breakpad提供的dump_syms工具,从release版本程序导出符号文件。当程序崩溃时,breakpad会捕捉崩溃,并生成dump文件。dump文件可以直接发送到指定服务器,或者由用户手动发给开发者。收到dump文件后,结合符号文件,可通过minidump_stackwalk工具生成堆栈调用信息文件,这个文件可以直接阅读,定位bug。
3、源码准备
我们知道qBreakpad是对Breakpad的封装,所以qBreakpad的编译,还依赖2套源码Breakpad、LSS。
因为github可能需要翻墙,所以我这里给出我收集好的所有源码码云连接,当然下面也会贴出github的源码原链接,有需要的可以自己去克隆或-也是一样
-Breakpad源码
-:Creator(Qt5.12.12) + MSVC (Vs2019)编译器。
将Breakpad、LSS源码放入third_party目录
克隆或解压qBreakpad源码后,在qBreakpad-master\third_party目录下,有如下2个目录,如下:
此时这两个目录是空的,需要分别克隆或解压Breakpad、LSS源码至breakpad和lss目录,此2个目录下源码需要参与qBreakpad的编译。放置好后,如下所示:
breakpad
LSS
qBreakpad工程介绍
在qBreakpad源码目录下,使用QtCreator打开qBreakpad.pro工程,如下:
demo工程下,有2个演示程序program和reporter,分别实现了演示生成dump文件,上报dump文件的功能。handler为静态库工程,该工程封装了Breakpad,直接编译此工程,可生成qBreakpad.lib。tests为一个简单的测试工程。
根据查阅网上参考说在源码中有3个bug,在编译前,我们需要先修正, 但是我直接一键编译构建没有任何错误,一步到位,不过也记录一下网上的错误吧!
所以,先自己构建一下,看有没有对应的错误,有的话,可以看下面的,没有直接跳过即可!
参考网上报错,源码bug修正(本人未报错)
编译生成qBreakpad.lib
进入主题,开始正式编译需要的库环境,首先分别在Debug、Release模式下,编译handler工程,生成2个版本的qBreakpad.lib静态库。
因为程序调用qBreakpad.lib时,只能debug版程序链接debug版库,release版程序链接release版库。debug版程序链接release版库会报错。
debug 编译
生产后的*.lib如下图:
这个时候需要拷贝到debug目录,然后重新清除或手动删除后,再编译release版本即可,不然可能就会覆盖
release 编译
生产的 *.lib 如下图:
对比debug生产的发现少了一个*.pdb文件, 说明release不会自动生产pdb文件,如何生产,我们后续测试程序会讲到,这里只需要拿到对应的*.lib文件即可!
5、在程序中调用qBreakpad
我们使用Qt新建一个名为qBreakpadTest 的简单QWidget程序,如下:
在工程目录下建立qbreakpadlib目录,用于存放lib和头文件。
然后,分别将debug版、release版qBreakpad.lib拷贝至,qbreakpadlib\lib\debug和qbreakpadlib\lib\release目录下。
再将调用库所需的头文件QBreakpadHandler.h、QBreakpadHttpUploader.h、call_once.h、singleton.h共4个文件拷贝至qbreakpadlib\include下。call_conce.h 和 singleton.h 在singletone文件夹目录下,一起连同文件夹目录拷贝到include下即可。
最后目录结构,如下:
在qBreakpadTest.pro文件中,添加如下内容:
############ for qBreakpad ############# qBreakpad中需要使用到network模块QT += network# 启用多线程、异常、RTTI、STL支持CONFIG += thread exceptions rtti stl# without c++11 & AppKit library compiler can't solve address for symbolsCONFIG += c++11macx: LIBS += -framework AppKit# 配置头文件搜索路径和链接库路径win32:CONFIG(release, debug|release): {LIBS += -L$$PWD/qbreakpadlib/lib/release/ -lqBreakpadDEPENDPATH += $$PWD/qbreakpadlib/lib/release} else:win32:CONFIG(debug, debug|release): {LIBS += -L$$PWD/qbreakpadlib/lib/debug/ -lqBreakpadDEPENDPATH += $$PWD/qbreakpadlib/lib/debug}INCLUDEPATH += $$PWD/qbreakpadlib/include############ for qBreakpad ############
然后在main.cpp中添加调用代码,如下:
#include "qBreakpadTest.h"#include
void qBreakpadTest::on_pushButton_crash_clicked(){ QLabel * a = nullptr; // 执行此句发生异常时,会自动生成dump文件 a->setText("会触发崩溃");}
6、生成dump文件
编译,运行程序,生成的dump文件,调试程序打印如下:
下面来看看具体生成的文件如下:
可以看到确实生成了dump文件,那么我们再来看看debug版本的pdb文件生成, 截图如下:
可以看到debug版本的dump和pdb都文件已经生成, 下面我们再生成release版本的dump和pdb, 首先Qt 切换到release版本模式,然后重新构建,如下图:
步骤和debug版本一致,先看程序调试信息:
说明已经成功捕获并生成了dump文件,下面我们在看看实际文件
可以发现,是没有pdb文件的生成的
前面我们说过需要dump和pdb文件才能进行更细致的定位bug。目前dump文件已经生成,release版本的pdb文件却没有生产,所以接下来了解如何生成release版本的pdb文件。
7、生成Release pdb文件
在debug模式下,默认就会生成pdb,但是我们期望的是,在release下也能生成pdb。毕竟交给客户的是release版,我们大多时候,也只是需要对release版程序进行bug定位。
其实关于这一点,在我前面有一篇专门的博客也讲解过QT如何在Release编译下生成pdb文件, 当然可以直接看下面的内容,都是一致的!
所以,需要在qBreakpadTest.pro文件中,添加如下内容,让release版程序带上调试信息:
QMAKE_CXXFLAGS_RELEASE = $$QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFOQMAKE_LFLAGS_RELEASE = $$QMAKE_LFLAGS_RELEASE_WITH_DEBUGINFO
win系统下,程序的调试信息,是在单独的pdb文件中;在其他linux、mac等系统下,程序的调试信息就包含在程序本体内部,所以带调试信息的程序一般比不带调试信息的大。
清除以后qmake,再次编译,可以看到,已经生成了qBreakpadTest.pdb, 如下图:
特别注意
MinGW是gcc在windows下的编译环境,GCC/MinGW以GNU GDB Debugger使用的格式生成调试信息,不支持Microsoft PDB格式。也就是说如果使用MinGW编译程序,无法生成pdb文件,这点需要注意一下。必须使用MSVC编译,方可生成pdb文件。如果是主程序+多个dll的开发方式,需要使用上述方法,将每个dll也生成pdb文件,这样,在dll中发生崩溃时,才能根据dump和pdb定位到dll的代码上。
目前我们已经生成了程序的pdb调试信息文件,并且程序执行过程中发生崩溃,也可以自动记录dump文件,这2个文件已经具备,接下来,我们看看如何利用他们定位到bug所在位置。
这里,其实有3种方式来,分析调试程序:
minidump_stackwalk,这是Breakpad提供的工具,可以用来生成,可读的堆栈调用信息。VS,微软提供的开发工具,使用最简单,缺点是安装过程较长。Windbg,微软提供的调试工具,使用稍麻烦,无需安装。
在win系统下,还是使用微软的工具,来的最方便,所以这里就不对minidump_stackwalk进行介绍了(后续我会单独开一篇关于如何使用minidump_stackwalk工具的针对性文章),下面主要对后两种进行说明。
8、使用VS进行调试
我这里使用VS 2019,来进行如下的操作。
打开dump文件
Vs文件菜单下,选择“打开”->“文件”,如下:(也可以直接选择拖拽dump文件到Vs也是一样)
找到dump文件,并打开,可以看到转储摘要和模块等,但是并不能发现问题何在。如下:
设置pdb文件路径
提示:
一般来说,我们只需要填写pdb所在的目录,不需要具体到pdb名称,因为根据dump文件,可以自动搜索到pdb文件。 尤其是对于主程序+多个dll的方式,就会存在多个pdb文件的情况,此时我们只设置目录,就可以方便的,自动从多个pdb文件中,找到对应的pdb。
进行调试
到此,我们顺利通过dump、pdb文件,成功定位到了bug所在。
注意,源码一定不能变化,哪怕只是更改加入了一行或者什么空格,都会导致定位错误或者只能定位大概位置,所以源码一定要和编译出exe时一致,下面看我的示例,我更改源码,然后只保存,不编译出exe,记住exe必须要和pdb生成时一致,不然定位就会报错,所以我更改源码,并没有重新生成exe,还是可以定位的,只不过会定位不准而已,如下图,我更改的源码位置:
此时可以发现,只能定位到是哪里调用的崩溃上一层了,如果源码更改过多,那么就绝对会定位BUG失败了!所以注意,源码、exe、pdb,三者要保持一样
下面介绍另外一种调试方式。
9、使用Windbg进行调试
第三方-windbg:环境搭建,主要针对MSVC/Android 平台) 建议使用微软
指定pdb文件路径
选择“File”->“Symbol File Path…”,如下:
直接输入pdb文件所在目录即可,它会自动找到适合的pdb文件。也可以输入pdb文件路径,若多个路径,则用分号分隔。
注意:
如果程序涉及到DLL,需要将EXE、所有涉及DLL的PDB路径都包括。
指定代码路径(可选)
选择“File”->“Source File Path…”,如下:
输入源文件路径。
这一步跳过是可以的,我是跳过了测试和不跳过测试结果一致, 不过设置一下也没什么问题
打开dump文件
选择“File”->“Open Crash Dump…”,如下:
选择dump文件,并打开,如下:
分析dump文件
输入`“!analyze -v”`,回车,开始进行分析。如下:
busy状态表示正在生成结果。最后生成的结果,如下:
STACK_TEXT表示调用堆栈信息。
网上有的可以显示 FAULTING_SOURCE_CODE 字段,其表示发生错误的源码,但是我这边并未显示
通过查看STACK_TEXT字段,根据堆栈也能判断出错误的地方
到此,我们使用windbg,顺利通过dump、pdb文件,成功定位到了bug所在。
10、dump文件上报
qBreakpad还提供了上报dump文件的方法。说白了就是,将生成的dump文件上传到指定的服务器。
上报演示程序,位于qBreakpad-master\demo\reporter下,感兴趣可以去看看。
使用也是十分简单。
class QBreakpadHandler: public QObject{ Q_OBJECTpublic: static QString version(); QBreakpadHandler(); ~QBreakpadHandler(); QString uploadUrl() const; QString dumpPath() const; QStringList dumpFileList() const; void setDumpPath(const QString& path); void setUploadUrl(const QUrl& url);public slots: void sendDumps();private: QBreakpadHandlerPrivate* d;};
基本流程:
先通过setDumpPath设置dump文件生成目录;以便在发生崩溃时,自动在该目录下生成dump文件。再通过setUploadUrl设置上报地址,以便后续将dump文件,上传到该地址。最后,通过sendDumps将dump文件发送至服务器。该函数会自动遍历,前面设置的dump生成目录,将每一个dump文件进行发送。
文件上传原理:QBreakpadHandler的sendDumps函数,使用QNetworkAccessManager的post()方法,来实现void (*p_callbackFun)(QString);class QBreakpadHandler: public QObject{ Q_OBJECTpublic: static QString version(); QBreakpadHandler(); ~QBreakpadHandler(); QString uploadUrl() const; QString dumpPath() const; QStringList dumpFileList() const; void setDumpPath(const QString& path); void setUploadUrl(const QUrl& url); //! 新增回调指针方法 void setCallbackMethod(p_callbackFun func){ m_callfunc = func; }; p_callbackFun m_callfunc; //!public slots: void sendDumps();private: QBreakpadHandlerPrivate* d;};
.cpp 文件修改如下:
#if defined(Q_OS_WIN32)bool DumpCallback(const wchar_t* dump_dir, const wchar_t* minidump_id, void* context, EXCEPTION_POINTERS* exinfo, MDRawAssertionInfo* assertion, bool succeeded)#elif defined(Q_OS_MAC)bool DumpCallback(const char *dump_dir, const char *minidump_id, void *context, bool succeeded)#elsebool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded)#endif{#ifdef Q_OS_LINUX Q_UNUSED(descriptor);#endif Q_UNUSED(context);#if defined(Q_OS_WIN32) Q_UNUSED(assertion); Q_UNUSED(exinfo);#endif /* NO STACK USE, NO HEAP USE THERE !!! Creating QString's, using qDebug, etc. - everything is crash-unfriendly. */#if defined(Q_OS_WIN32) QString path = QString::fromWCharArray(dump_dir) + QLatin1String("/") + QString::fromWCharArray(minidump_id); qDebug("%s, dump path: %s\n", succeeded ? "Succeed to write minidump" : "Failed to write minidump", qPrintable(path));#elif defined(Q_OS_MAC) QString path = QString::fromUtf8(dump_dir) + QLatin1String("/") + QString::fromUtf8(minidump_id); qDebug("%s, dump path: %s\n", succeeded ? "Succeed to write minidump" : "Failed to write minidump", qPrintable(path));#else qDebug("%s, dump path: %s\n", succeeded ? "Succeed to write minidump" : "Failed to write minidump", descriptor.path());#endif //! 调用外部的回调函数 QBreakpadInstance.m_callfunc(path); return succeeded;}
下面看实战,重新编译出*.lib文件, 拷贝新的QBreakpadHandler.h头文件到我们上个qBreakpadTest 工程中,然后代码在main.cpp中调用我们新增的回调接口,看在崩溃时,能否调用到我们自己写的回调方法中去,并做一些事情。新增工程代码如下:
int main(int argc, char *argv[]){ QApplication a(argc, argv); QBreakpadInstance.setDumpPath("crashes"); // 设置生成dump文件路径 qBreakpadTest w; //! 调用我们新增的回调方法,让崩溃时qbreakpad能调用我们自己实现的onBreakpadCrash方法逻辑 QBreakpadInstance.setCallbackMethod(&qBreakpadTest::onBreakpadCrash); w.show(); return a.exec();}# onBreakpadCrash实现如下void qBreakpadTest::onBreakpadCrash(QString dumpPath){ qInfo()<<"捕获到崩溃,现在准备调用发送dump文件到服务端程序~, dump文件路径是:"< 运行程序后打印如下: 可以看到,在程序崩溃之前,是可以触发到我们程序需要解决的逻辑应用环节中去的,此时,我们可以调用另外的崩溃上传文件程序,告知用户崩溃了,是否发送崩溃信息日志等文件功能! 12、总结 我们可以在自己的程序中,借助qBreakpad,很容易实现跨平台的,dump文件生成。 对于在程序中集成qBreakpad,实际就是,在程序中调用qBreakpad的静态库而已,非常的简单。 对于程序生成的dump文件,可以由用户直接发给我们,也可以由程序自动上报到我们的服务器上。 然后,我们拿到dump和pdb文件,借助VS或者Windbg,就可以快速定位bug。 特别注意: 欲定位bug,至少需要dump和pdb,这2个文件。 pdb文件与生成dump的程序必须配套,即同一次编译生成的。即使代码没有变化,重新编译生成的pdb都是不行的。所以请妥善保管好发布程序的pdb文件。 原因如下: 调试器是如何来判别EXE、DLL等是否和一个pdb文件匹配呢? 每次我们链接EXE或者DLL的时候,链接器都将产生一个唯一的GUID,然后将其写入到PDB和可执行文件。调试器加载的时候将检查两者的GUID,如果一致就表示他们匹配。 注:如果我们需要调试,我们需要查dump文件,那么请妥善保管好自己的代码和pdb。每次重新编译,即使所有代码均没有变化,他们的GUID也不同。 文章参考于Windows下Qt生成dump文件并定位bug(基于qBreakpad), 谢谢观看! 下一篇 Qt Linux系统使用QBreakpad实战 待写
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~