当我们写的程序跑在客户的机器上,因为一个bug,导致程序崩溃,你会有些什么办法来,定位并修复这个bug呢?
有人会说记录日志,即便有日志,也是不好定位的,因为你只能推测出大概的模块或者位置,无法定位到具体出错的代码行。
此时,我们可以让程序崩溃后,自动生成一个*.dmp文件,并配合在编译该程序时生成的pdb文件,来准确定位到调用堆栈、代码行上。这样很轻易就可以找到该bug。
微软有成熟的工具可以分析,比如VS和windbg。所以我们当务之急,就是准备好这2个文件,后面再说如何分析,其实比较简单,耐心就好。
在不同的平台下,生成dump文件的方式都不同。既然用上Qt了,自然是想要任何东西都最好跨平台。
经过一番调查,发现有一个来自谷歌的开源项目叫Breakpad,统一了这三平台win、linux、mac生成dump的方式,通过它就可以跨平台。
Github地址:https://github.com/google/breakpad
使用也是相对简单的,大概就是下载源码,编译生成lib和dll,然后在你自己的程序中include头文件,就可以在你的程序中集成,在崩溃时生成dump文件。
在我查找Breakpad相关文章时,又发现了一个开源项目叫qBreakpad,这玩意,腻害了,直接将懒癌进行到底,使用Qt对Breakpad进一步封装,使用更简单了。
Github地址:https://github.com/buzzySmile/qBreakpad
大致了解了下qBreakpad,该源码简单到无以复加,虽github上文档有些年久失修,但是考虑到如此简单,也就无关痛痒了。
俗话说,站在巨人的肩上看得更远。接下来,我们就选择qBreakpad来生成dump文件吧。
我们大概先了解下Breakpad的一些常识。
Breakpad是Google公司开发的开源多平台C++崩溃检测库。Breakpad可以捕获发布给用户的应用程序的崩溃,并记录软件崩溃的调试信息到“minidump”文件中,即*.dmp。
minidump是由微软开发的崩溃记录文件格式。minidump为二进制文件,体积小。为了保持统一,Breakpad在其他系统下也选择生成minidump文件。
除此之外,Breakpad还可以调试信息包括错误行号,报错详情,堆栈错误(stack traces)。支持软件崩溃时候把生成的dump文件上传到自己的服务器上就可以方便的获取崩溃详情。
支持的平台:windows、linux、mac、ios、solaris、android ndk
在不同平台下的实现原理:
BreakPad工作原理示意图:
表达的意思就是:
我们知道qBreakpad是对Breakpad的封装,所以qBreakpad的编译,还依赖2套源码Breakpad、LSS。
下载地址:https://github.com/google/breakpad
下载地址:https://github.com/ithaibo/linux-syscall-support
下载地址:https://github.com/buzzySmile/qBreakpad
以下开发环境:Win10下,Qt Creator + MSVC编译器。
解压qBreakpad源码后,在qBreakpad-master\third_party目录下,有如下2个目录,如下:
分别解压Breakpad、LSS源码至breakpad和lss目录,此2个目录下源码需要参与qBreakpad的编译。放置好后,如下所示:
在qBreakpad源码目录下,使用QtCreator打开qBreakpad.pro工程,如下:
demo工程下,有2个演示程序program和reporter,分别实现了演示生成dump文件,上报dump文件的功能。
handler为静态库工程,该工程封装了Breakpad,直接编译此工程,可生成qBreakpad.lib。
tests为一个简单的测试工程。
但是在源码中有3个bug,在编译前,我们需要先修正。
I、在Debug模式下编译demo工程时,报错
报错如下:检测到“RuntimeLibrary”的不匹配项:值“MD_DynamicRelease”不匹配值“MDd_DynamicDebug”(TestThread.obj 中)
解决办法: 在qBreakpad-master/config.pri文件中,删除CONFIG += release
此行,重新编译handler工程,再编译demo工程,错误消失。
II、编译demo工程时,报错
报错如下:error: LNK1104: 无法打开文件“qBreakpad.lib”
解决办法:在qBreakpad-master\demo\reporter\reporter.pro文件中,添加如下一行,
QMAKE_LIBDIR += $$OUT_PWD/../../handler
再次编译demo工程,错误消失。
III、编译tests工程时,函数返回值报错
解决办法:在qBreakpad-master\tests\duplicates\main.cpp文件中,为各个函数添加返回值即可。
这3个报错,其实都比较简单,但是为了方便,我这里提供一个集成了Breakpad、LSS、qBreakpad源码,并修正了bug的压缩包,供大家下载,可以直接在win、linux下编译通过。
链接: https://pan.baidu.com/s/1UHgikyipUZyEygJ7AKTBzg
提取码: 6jki
分别在Debug、Release模式下,编译handler工程,生成2个版本的qBreakpad.lib静态库。
因为程序调用qBreakpad.lib时,只能debug版程序链接debug版库,release版程序链接release版库。debug版程序链接release版库会报错。
我们建立qBreakpadTest控制台程序,如下:
在工程目录下建立qBreakpad目录,用于存放lib和头文件。
然后,分别将debug版、release版qBreakpad.lib拷贝至,qBreakpad\lib\debug和qBreakpad\lib\release目录下。
再将调用库所需的头文件QBreakpadHandler.h、QBreakpadHttpUploader.h、call_once.h、singleton.h共4个文件拷贝至qBreakpad\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 symbols
CONFIG += c++11
macx: LIBS += -framework AppKit
# 配置头文件搜索路径和链接库路径
INCLUDEPATH += $$PWD/qBreakpad/include
CONFIG(debug, debug|release) {
LIBS += -L$$PWD/qBreakpad/lib/debug -lqBreakpad
} else {
LIBS += -L$$PWD/qBreakpad/lib/release -lqBreakpad
}
############ for qBreakpad ############
然后在main.cpp中添加调用代码,如下:
#include
#include "QBreakpadHandler.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QBreakpadInstance.setDumpPath("crashes"); // 设置生成dump文件路径
// 执行此句发生异常时,会自动生成dump文件
*((int*)0) = 10;
return a.exec();
}
编译,运行程序,生成的dump文件,如下:
前面我们说过需要dump和pdb文件才能进行更细致的定位bug。
目前dump文件已经生成,接下来了解如何生成pdb文件。
在debug模式下,默认就会生成pdb,但是我们期望的是,在release下也能生成pdb。毕竟交给客户的是release版,我们大多时候,也只是需要对release版程序进行bug定位。
所以,需要在qBreakpadTest.pro文件中,添加如下内容,让release版程序带上调试信息:
QMAKE_CXXFLAGS_RELEASE = $$QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO
QMAKE_LFLAGS_RELEASE = $$QMAKE_LFLAGS_RELEASE_WITH_DEBUGINFO
win系统下,程序的调试信息,是在单独的pdb文件中;在其他linux、mac等系统下,程序的调试信息就包含在程序本体内部,所以带调试信息的程序一般比不带调试信息的大。
再次编译,可以看到,已经生成了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种方式来,分析调试程序:
在win系统下,还是使用微软的工具,来的最方便,所以就不对minidump_stackwalk进行介绍了,主要下面对后2种进行说明。
我这里使用VS 2017,来进行如下的操作。
1、打开dump文件
文件菜单下,选择“打开”->“文件”,如下:
找到dump文件,并打开,可以看到转储摘要和模块等,但是并不能发现问题何在。如下:
2、设置pdb文件路径
接下来,点击“设置符号路径”,点击“+”号,添加pdb文件路径,之后,“确定”。如下:
提示:
一般来说,我们只需要填写pdb所在的目录,不需要具体到pdb名称,因为根据dump文件,可以自动搜索到pdb文件。
尤其是对于主程序+多个dll的方式,就会存在多个pdb文件的情况,此时我们只设置目录,就可以方便的,自动从多个pdb文件中,找到对应的pdb。
3、进行调试
最后,点击“使用 仅限本机 进行调试”,可以很清楚的,定位到崩溃发生的代码行上。如下:
到此,我们顺利通过dump、pdb文件,成功定位到了bug所在。
下面介绍另外一种调试方式。
下载windbg:https://dl.pconline.com.cn/download/770876-1.html
先解压,然后运行WinDbg(x64)\windbg.exe。
1、指定pdb文件路径
选择“File”->“Symbol File Path…”,如下:
直接输入pdb文件所在目录即可,它会自动找到适合的pdb文件。也可以输入pdb文件路径,若多个路径,则用分号分隔。
注意:
如果程序涉及到DLL,需要将EXE、所有涉及DLL的PDB路径都包括。
2、指定代码路径(可选)
选择“File”->“Source File Path…”,如下:
输入源文件路径。
3、打开dump文件
选择“File”->“Open Crash Dump…”,如下:
选择dump文件,并打开,如下:
4、分析dump文件
输入“!analyze -v”,回车,开始进行分析。如下:
busy状态表示正在生成结果。最后生成的结果,如下:
我们把内容拉到最右边,查看完整的STACK_TEXT信息,如下:
堆栈指示异常发生于main.cpp的第11行,与FAULTING_SOURCE_CODE段内容不谋而合。
注意:
若前面的源文件路径不填写,则最后分析生成结果中,只会存在STACK_TEXT堆栈信息,不会存在FAULTING_SOURCE_CODE字段。但是即便如此,仅凭堆栈信息,也可以定位到bug位于main.cpp的第11行。
到此,我们使用windbg,顺利通过dump、pdb文件,成功定位到了bug所在。
qBreakpad还提供了上报dump文件的方法。说白了就是,将生成的dump文件上传到指定的服务器。
上报演示程序,位于qBreakpad-master\demo\reporter下,感兴趣可以去看看。
使用也是十分简单。
class QBreakpadHandler: public QObject
{
Q_OBJECT
public:
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;
};
基本流程:
文件上传原理:QBreakpadHandler的sendDumps函数,使用QNetworkAccessManager的post()方法,来实现http协议方式的,文件上传。
上报功能,根据自身的需求,来确定有没有必要。此处不再举例说明。
我们可以在自己的程序中,借助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也不同。
本文涉及工程代码:
https://gitee.com/bailiyang/cdemo/tree/master/Qt/57qBreakpadTest/qBreakpadTest
若对你有帮助,欢迎点赞、收藏、评论,你的支持就是我的最大动力!!!
同时,阿超为大家准备了丰富的学习资料,欢迎关注公众号“超哥学编程”,即可领取。