更多精彩内容,请关注我的公众号
在这里插入图片描述

前言

最近想研究下Qt下跨平台的崩溃捕获,经过一番调查,发现有一个来自谷歌的开源项目叫Breakpad,统一了这三平台win、linux、mac生成dump的方式,通过它就可以跨平台。

使用也是相对简单的,大概就是下载源码,编译生成lib和dll,然后在你自己的程序中include头文件,就可以在你的程序中集成,在崩溃时生成dump文件。

在我查找Breakpad相关文章时,又发现了一个开源项目叫qBreakpad,这玩意,腻害了,直接将懒癌进行到底,使用QtBreakpad进一步封装,使用更简单了。

大致了解了下qBreakpad,该源码简单到无以复加,虽github上文档有些年久失修,但是考虑到如此简单,也就无关痛痒了。

俗话说,站在巨人的肩上看得更远。接下来,我们就选择qBreakpad来生成dump文件吧。

1、dump和pdb是什么

当我们写的程序跑在客户的机器上,因为一个bug,导致程序崩溃,你会有些什么办法来,定位并修复这个bug呢?

有人会说记录日志,即便有日志,也是不好定位的,因为你只能推测出大概的模块或者位置,无法定位到具体出错的代码行。

此时,我们可以让程序崩溃后,自动生成一个*.dmp文件,并配合在编译该程序时生成的pdb文件,来准确定位到调用堆栈、代码行上。这样很轻易就可以找到该bug

  • dump文件,后缀 *.dmp,是程序崩溃时的内存转储文件;
  • pdb文件,后缀 *.pdb,是程序的符号文件。

微软有成熟的工具可以分析,比如VSwindbg。所以我们当务之急,就是准备好这2个文件,后面再说如何分析,其实比较简单,耐心就好。

2、Breakpad介绍

当我们写的程序跑在客户的机器上,因为一个bug,导致程序崩溃,你会有些什么办法来,定位修复这个bug呢?

我们大概先了解下Breakpad的一些常识。

BreakpadGoogle公司开发的开源多平台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套源码BreakpadLSS

因为github可能需要翻墙,所以我这里给出我收集好的所有源码码云连接,当然下面也会贴出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目录

克隆或解压qBreakpad源码后,在qBreakpad-master\third_party目录下,有如下2个目录,如下:
在这里插入图片描述
此时这两个目录是空的,需要分别克隆或解压BreakpadLSS源码至breakpadlss目录,此2个目录下源码需要参与qBreakpad的编译。放置好后,如下所示:
breakpad
在这里插入图片描述
LSS
在这里插入图片描述

qBreakpad工程介绍

qBreakpad源码目录下,使用QtCreator打开qBreakpad.pro工程,如下:
在这里插入图片描述

  • demo工程下,有2个演示程序program和reporter,分别实现了演示生成dump文件,上报dump文件的功能。

  • handler为静态库工程,该工程封装了Breakpad,直接编译此工程,可生成qBreakpad.lib

  • tests为一个简单的测试工程

根据查阅网上参考说在源码中有3个bug,在编译前,我们需要先修正, 但是我直接一键编译构建没有任何错误,一步到位,不过也记录一下网上的错误吧!

所以,先自己构建一下,看有没有对应的错误,有的话,可以看下面的,没有直接跳过即可!

参考网上报错,源码bug修正(本人未报错, 可以忽略跳过)

  1. 在Debug模式下编译demo工程时,报错
    报错如下:检测到“RuntimeLibrary”的不匹配项:值“MD_DynamicRelease”不匹配值“MDd_DynamicDebug”(TestThread.obj 中)

    解决办法: 在qBreakpad-master/config.pri文件中,删除CONFIG += release此行,重新编译handler工程,再编译demo工程,错误消失。

  2. 编译demo工程时,报错
    报错如下:error: LNK1104: 无法打开文件“qBreakpad.lib”

    解决办法:在qBreakpad-master\demo\reporter\reporter.pro文件中,添加如下一行,

    QMAKE_LIBDIR += $$OUT_PWD/../../handler
    

    再次编译demo工程,错误消失。

  3. 编译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版、releaseqBreakpad.lib拷贝至,qbreakpadlib\lib\debugqbreakpadlib\lib\release目录下。

再将调用库所需的头文件QBreakpadHandler.h、QBreakpadHttpUploader.h、call_once.h、singleton.h共4个文件拷贝至qbreakpadlib\include下。call_conce.hsingleton.hsingletone文件夹目录下,一起连同文件夹目录拷贝到include下即可。

最后目录结构,如下:
在这里插入图片描述
qBreakpadTest.pro文件中,添加如下内容:

############ for qBreakpad ############
# qBreakpad中需要使用到network模块
QT += network

# 启用多线程、异常、RTTISTL支持
CONFIG += thread exceptions rtti stl

# without c++11 & AppKit library compiler can't solve address for symbols
CONFIG += c++11
macx: LIBS += -framework AppKit

# 配置头文件搜索路径和链接库路径
win32:CONFIG(release, debug|release): {
LIBS += -L$$PWD/qbreakpadlib/lib/release/ -lqBreakpad
DEPENDPATH += $$PWD/qbreakpadlib/lib/release
} 
else:win32:CONFIG(debug, debug|release): {
LIBS += -L$$PWD/qbreakpadlib/lib/debug/ -lqBreakpad
DEPENDPATH += $$PWD/qbreakpadlib/lib/debug
}

INCLUDEPATH += $$PWD/qbreakpadlib/include

############ for qBreakpad ############

然后在main.cpp中添加调用代码,如下:

#include "qBreakpadTest.h"
#include <QApplication>

#include "QBreakpadHandler.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    
    QBreakpadInstance.setDumpPath("crashes");    // 设置生成dump文件路径
    qBreakpadTest w;
    w.show();
    return a.exec();
}

然后再qBreakpadTest.cpp 中加入一个按钮,点击按钮后,我们调用一个空指针,使程序崩溃,从而生产dump文件,代码如下:

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文件,下面我们在看看实际文件
在这里插入图片描述可以看到确实生成了dump文件了,再看看pdb文件,如下图所示:
在这里插入图片描述
可以发现,是没有pdb文件的生成的

前面我们说过需要dumppdb文件才能进行更细致的定位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_DEBUGINFO
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文件路径

选择“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_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;
};

基本流程:

  • 先通过setDumpPath设置dump文件生成目录;以便在发生崩溃时,自动在该目录下生成dump文件。
  • 再通过setUploadUrl设置上报地址,以便后续将dump文件,上传到该地址。
  • 最后,通过sendDumpsdump文件发送至服务器。该函数会自动遍历,前面设置的dump生成目录,将每一个dump文件进行发送。

文件上传原理:QBreakpadHandlersendDumps函数,使用QNetworkAccessManagerpost()方法,来实现http协议方式的,文件上传。

上报功能,根据自身的需求,来确定有没有必要。此处不再举例说明。

11、获取崩溃时的通知

程序崩溃了,我们要想程序第一时间知道,使用qbreakpad的话,就得修改源码,如果使用纯breakpad源码接口,就要自己重写捕获崩溃的回调接口函数,这样肯定就能知道,但是qbreakpad是封装了一层了,已经把崩溃捕获回调实现了,所以如果想自己也第一时间拿到崩溃的通知,就得在qbreakpad崩溃回调内部再回调外部的函数指针接口即可,看下面的实现。(新增信号触发是无效,已经测试过了,必须使用函数指针才行哦)

.h文件新增如下:

typedef void (*p_callbackFun)(QString);

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);

    //! 新增回调指针方法
    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)
#else
bool 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文件路径是:"<<dumpPath;
    //todo ...
}

运行程序后打印如下:
在这里插入图片描述
可以看到,在程序崩溃之前,是可以触发到我们程序需要解决的逻辑应用环节中去的,此时,我们可以调用另外的崩溃上传文件程序,告知用户崩溃了,是否发送崩溃信息日志等文件功能!

12、总结

我们可以在自己的程序中,借助qBreakpad,很容易实现跨平台的,dump文件生成。

对于在程序中集成qBreakpad,实际就是,在程序中调用qBreakpad的静态库而已,非常的简单。

对于程序生成的dump文件,可以由用户直接发给我们,也可以由程序自动上报到我们的服务器上。

然后,我们拿到dump和pdb文件,借助VS或者Windbg,就可以快速定位bug。

特别注意:
欲定位bug,至少需要dumppdb,这2个文件。

pdb文件与生成dump的程序必须配套,即同一次编译生成的。即使代码没有变化,重新编译生成的pdb都是不行的。所以请妥善保管好发布程序的pdb文件。

原因如下:

调试器是如何来判别EXE、DLL等是否和一个pdb文件匹配呢?
每次我们链接EXE或者DLL的时候,链接器都将产生一个唯一的GUID,然后将其写入到PDB和可执行文件。调试器加载的时候将检查两者的GUID,如果一致就表示他们匹配。
注:如果我们需要调试,我们需要查dump文件,那么请妥善保管好自己的代码和pdb。每次重新编译,即使所有代码均没有变化,他们的GUID也不同。

文章参考于Windows下Qt生成dump文件并定位bug(基于qBreakpad), 谢谢观看!

下一篇
Qt Linux系统使用QBreakpad实战

更多精彩内容,请关注我的公众号
在这里插入图片描述

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐