前言
最近想研究下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可能需要FQ,所以我这里给出我收集好的所有源码码云连接,当然下面也会贴出github的源码原链接,有需要的可以自己去克隆或下载也是一样
下载Breakpad源码
下载地址:https://github.com/google/breakpad
下载LSS源码
下载地址:https://github.com/ithaibo/linux-syscall-support
下载qBreakpad源码
下载地址:https://github.com/buzzySmile/qBreakpad
4、编译qBreakpad
以下开发环境:Win10下,Qt Creator(Qt5.12.12) + MSVC (Vs2019)编译器。
将Breakpad、LSS源码放入third_party目录
此时这两个目录是空的
,需要分别克隆或解压Breakpad
、LSS
源码至breakpad
和lss
目录,此2个目录下源码需要参与qBreakpad
的编译。放置好后,如下所示:
breakpad
LSS
qBreakpad工程介绍
- demo工程下,有2个演示程序program和reporter,分别实现了演示生成dump文件,上报dump文件的功能。
- handler为静态库工程,该工程封装了Breakpad,直接编译此工程,可生成qBreakpad.lib。
- tests为一个简单的测试工程。
根据查阅网上参考说在源码中有3个bug,在编译前,我们需要先修正, 但是我直接一键编译构建没有任何错误,一步到位,不过也记录一下网上的错误吧!
所以,先自己构建一下,看有没有对应的错误,有的话,可以看下面的,没有直接跳过即可!
参考网上报错,源码bug修正(本人未报错, 可以忽略跳过)
在Debug模式下编译demo工程时,报错
报错如下:检测到“RuntimeLibrary”的不匹配项:值“MD_DynamicRelease”不匹配值“MDd_DynamicDebug”(TestThread.obj 中)
解决办法: 在qBreakpad-master/config.pri文件中,删除CONFIG += release此行,重新编译handler工程,再编译demo工程,错误消失。
编译demo工程时,报错
报错如下:error: LNK1104: 无法打开文件“qBreakpad.lib”
解决办法:在qBreakpad-master\demo\reporter\reporter.pro文件中,添加如下一行,
再次编译demo工程,错误消失。
编译tests工程时,函数返回值报错
报错如下:
解决办法:在qBreakpad-master\tests\duplicates\main.cpp文件中,为各个函数添加返回值即可。
编译生成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
文件中,添加如下内容:
1 ############ for qBreakpad ############
2 # qBreakpad中需要使用到network模块
3 QT += network
4
5 # 启用多线程、异常、RTTI、STL支持
6 CONFIG += thread exceptions rtti stl
7
8 # without c++11 & AppKit library compiler can't solve address for symbols
9 CONFIG += c++11
10 macx: LIBS += -framework AppKit
11
12 # 配置头文件搜索路径和链接库路径
13 win32:CONFIG(release, debug|release): {
14 LIBS += -L$$PWD/qbreakpadlib/lib/release/ -lqBreakpad
15 DEPENDPATH += $$PWD/qbreakpadlib/lib/release
16 }
17 else:win32:CONFIG(debug, debug|release): {
18 LIBS += -L$$PWD/qbreakpadlib/lib/debug/ -lqBreakpad
19 DEPENDPATH += $$PWD/qbreakpadlib/lib/debug
20 }
21
22 INCLUDEPATH += $$PWD/qbreakpadlib/include
23
24 ############ for qBreakpad ############
然后在main.cpp
中添加调用代码,如下:
1 #include "qBreakpadTest.h"
2 #include <QApplication>
3
4 #include "QBreakpadHandler.h"
5
6 int main(int argc, char *argv[])
7 {
8 QApplication a(argc, argv);
9
10 QBreakpadInstance.setDumpPath("crashes"); // 设置生成dump文件路径
11 qBreakpadTest w;
12 w.show();
13 return a.exec();
14 }
然后再qBreakpadTest.cpp
中加入一个按钮,点击按钮后,我们调用一个空指针,使程序崩溃,从而生产dump文件,代码如下:
1 void qBreakpadTest::on_pushButton_crash_clicked()
2 {
3 QLabel * a = nullptr;
4
5 // 执行此句发生异常时,会自动生成dump文件
6 a->setText("会触发崩溃");
7 }
6、生成dump文件
编译,运行程序,生成的dump
文件,调试程序打印如下:
下面来看看具体生成的文件如下:
可以看到确实生成了dump文件,那么我们再来看看debug版本的pdb文件生成, 截图如下:
可以看到debug版本的dump和pdb都文件已经生成, 下面我们再生成release版本的dump和pdb, 首先Qt 切换到release版本模式,然后重新构建,如下图:
步骤和debug版本一致,先看程序调试信息:
说明已经成功捕获并生成了dump文件,下面我们在看看实际文件
到确实生成了dump文件了,再看看pdb文件,如下图所示:
可以发现,是没有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版程序带上调试信息:
1 QMAKE_CXXFLAGS_RELEASE = $$QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO
2 QMAKE_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所在的目录,不需要具体到pdb名称,因为根据dump文件,可以自动搜索到pdb文件。
尤其是对于主程序+多个dll的方式,就会存在多个pdb文件的情况,此时我们只设置目录,就可以方便的,自动从多个pdb文件中,找到对应的pdb。
进行调试
最后,点击“使用 仅限本机 进行调试”,可以很清楚的,定位到崩溃发生的代码行上。如下:
到此,我们顺利通过dump、pdb文件,成功定位到了bug所在。
注意,源码一定不能变化,哪怕只是更改加入了一行或者什么空格,都会导致定位错误或者只能定位大概位置,所以源码一定要和编译出exe时一致,下面看我的示例,我更改源码,然后只保存,不编译出exe,记住exe必须要和pdb生成时一致,不然定位就会报错,所以我更改源码,并没有重新生成exe,还是可以定位的,只不过会定位不准而已,如下图,我更改的源码位置:
新增打印语句,然后我们重新再次加载dump文件,然后点击本地调试如下图所示:
此时可以发现,只能定位到是哪里调用的崩溃上一层了,如果源码更改过多,那么就绝对会定位BUG失败了!所以注意,源码、exe、pdb,三者要保持一样
下面介绍另外一种调试方式。
9、使用Windbg进行调试
第三方下载windbg:https://dl.pconline.com.cn/download/770876-1.html
下载好windb后先解压,然后运行WinDbg(x64)\windbg.exe。
微软官网下载Windb和安装可以看我这篇博客Qt系列文章之二(Qt 环境搭建,主要针对MSVC/Android 平台) 建议使用微软
指定pdb文件路径
直接输入pdb文件
所在目录即可,它会自动找到适合的pdb文件。也可以输入pdb文件路径,若多个路径,则用分号分隔
。
注意:
如果程序涉及到DLL,需要将EXE、所有涉及DLL的PDB路径都包括。
指定代码路径(可选)
选择“File”
->“Source File Path…”
,如下:
输入源文件路径。
这一步跳过是可以的,我是跳过了测试和不跳过测试结果一致, 不过设置一下也没什么问题
打开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下,感兴趣可以去看看。
使用也是十分简单。
1 class QBreakpadHandler: public QObject
2 {
3 Q_OBJECT
4 public:
5 static QString version();
6
7 QBreakpadHandler();
8 ~QBreakpadHandler();
9
10 QString uploadUrl() const;
11 QString dumpPath() const;
12 QStringList dumpFileList() const;
13
14 void setDumpPath(const QString& path);
15 void setUploadUrl(const QUrl& url);
16
17 public slots:
18 void sendDumps();
19
20 private:
21 QBreakpadHandlerPrivate* d;
22 };
基本流程:
先通过setDumpPath设置dump文件生成目录;以便在发生崩溃时,自动在该目录下生成dump文件。
再通过setUploadUrl设置上报地址,以便后续将dump文件,上传到该地址。
最后,通过sendDumps将dump文件发送至服务器。该函数会自动遍历,前面设置的dump生成目录,将每一个dump文件进行发送。
文件上传原理:QBreakpadHandler的sendDumps函数,使用QNetworkAccessManager的post()方法,来实现http协议方式的,文件上传。
上报功能,根据自身的需求,来确定有没有必要。此处不再举例说明。
11、获取崩溃时的通知
程序崩溃了,我们要想程序第一时间知道,使用qbreakpad的话,就得修改源码,如果使用纯breakpad源码接口,就要自己重写捕获崩溃的回调接口函数,这样肯定就能知道,但是qbreakpad是封装了一层了,已经把崩溃捕获回调实现了,所以如果想自己也第一时间拿到崩溃的通知,就得在qbreakpad崩溃回调内部再回调外部的函数指针接口即可,看下面的实现。(新增信号触发是无效,已经测试过了,必须使用函数指针才行哦)
.h文件新增如下:
1 typedef void (*p_callbackFun)(QString);
2
3 class QBreakpadHandler: public QObject
4 {
5 Q_OBJECT
6 public:
7 static QString version();
8
9 QBreakpadHandler();
10 ~QBreakpadHandler();
11
12 QString uploadUrl() const;
13 QString dumpPath() const;
14 QStringList dumpFileList() const;
15
16 void setDumpPath(const QString& path);
17 void setUploadUrl(const QUrl& url);
18
19 //! 新增回调指针方法
20 void setCallbackMethod(p_callbackFun func){
21 m_callfunc = func;
22 };
23 p_callbackFun m_callfunc;
24 //!
25
26 public slots:
27 void sendDumps();
28
29 private:
30 QBreakpadHandlerPrivate* d;
31 };
cpp 文件修改如下:
1 #if defined(Q_OS_WIN32)
2 bool DumpCallback(const wchar_t* dump_dir,
3 const wchar_t* minidump_id,
4 void* context,
5 EXCEPTION_POINTERS* exinfo,
6 MDRawAssertionInfo* assertion,
7 bool succeeded)
8 #elif defined(Q_OS_MAC)
9 bool DumpCallback(const char *dump_dir,
10 const char *minidump_id,
11 void *context, bool succeeded)
12 #else
13 bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
14 void* context,
15 bool succeeded)
16 #endif
17 {
18 #ifdef Q_OS_LINUX
19 Q_UNUSED(descriptor);
20 #endif
21 Q_UNUSED(context);
22 #if defined(Q_OS_WIN32)
23 Q_UNUSED(assertion);
24 Q_UNUSED(exinfo);
25 #endif
26 /*
27 NO STACK USE, NO HEAP USE THERE !!!
28 Creating QString's, using qDebug, etc. - everything is crash-unfriendly.
29 */
30
31 #if defined(Q_OS_WIN32)
32 QString path = QString::fromWCharArray(dump_dir) + QLatin1String("/") + QString::fromWCharArray(minidump_id);
33 qDebug("%s, dump path: %s\n", succeeded ? "Succeed to write minidump" : "Failed to write minidump", qPrintable(path));
34 #elif defined(Q_OS_MAC)
35 QString path = QString::fromUtf8(dump_dir) + QLatin1String("/") + QString::fromUtf8(minidump_id);
36 qDebug("%s, dump path: %s\n", succeeded ? "Succeed to write minidump" : "Failed to write minidump", qPrintable(path));
37 #else
38 qDebug("%s, dump path: %s\n", succeeded ? "Succeed to write minidump" : "Failed to write minidump", descriptor.path());
39 #endif
40
41 //! 调用外部的回调函数
42 QBreakpadInstance.m_callfunc(path);
43 return succeeded;
44 }
下面看实战,重新编译出*.lib
文件, 拷贝新的QBreakpadHandler.h
头文件到我们上个qBreakpadTest
工程中,然后代码在main.cpp
中调用我们新增的回调接口
,看在崩溃时,能否调用到我们自己写的回调方法中去,并做一些事情。新增工程代码如下:
1 int main(int argc, char *argv[])
2 {
3 QApplication a(argc, argv);
4
5 QBreakpadInstance.setDumpPath("crashes"); // 设置生成dump文件路径
6
7
8 qBreakpadTest w;
9 //! 调用我们新增的回调方法,让崩溃时qbreakpad能调用我们自己实现的onBreakpadCrash方法逻辑
10 QBreakpadInstance.setCallbackMethod(&qBreakpadTest::onBreakpadCrash);
11 w.show();
12 return a.exec();
13 }
14
15 # onBreakpadCrash实现如下
16 void qBreakpadTest::onBreakpadCrash(QString dumpPath)
17 {
18 qInfo()<<"捕获到崩溃,现在准备调用发送dump文件到服务端程序~, dump文件路径是:"<<dumpPath;
19 //todo ...
20 }
运行程序后打印如下:
可以看到,在程序崩溃之前,是可以触发到我们程序需要解决的逻辑应用环节中去的,此时,我们可以调用另外的崩溃上传文件程序,告知用户崩溃了,是否发送崩溃信息日志等文件功能!
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也不同。