02-跨平台的Qt程序崩溃之前生成Dump文件Breakpad

02-跨平台的Qt程序崩溃之前生成Dump文件Breakpad

2019-06-14 09:16:45 robert_cysy 阅读数 201更多

分类专栏: Qt

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/robert_cysy/article/details/91948025

一、崩溃捕捉记录的意义

应用程序发布出去之后用户使用的过程中出现崩溃的情况下,软件需要记录崩溃详情并给用户弹出崩溃提示对话框,提示用户重启软件以及上报(上传)崩溃。
这样做能保证软件在使用过程中发现的bug都能很好的收集起来,并根据崩溃详情修改bug,然后发布修复bug的版本。

如果软件在使用过程中直接闪退没有任何提示,不仅用户体验不好,还无法获取崩溃的原因。之后更具用户反馈的口头描述来找bug就会非常头疼了。

二、在Qt开发中怎样实现崩溃捕捉记录呢?

开发c#应用程序的时候用Log4Net库就可以轻松的获取崩溃后的软件崩溃详情。如果还附带应用程序对应的pdb调试文件Exception报错记录里会有出现崩溃代码的具体行号,非常方便。但是现在开发Qt开发的时候怎样实现呢?

  1. 如果开发的Qt应用程序只在Windows系统下运行,可以使用“SetUnhandledExceptionFilter”来设置应用程序奔溃捕捉,这样软件在崩溃的时候,这个函数指定的回掉函数就会被调用并返回崩溃详情。返回的崩溃详情为MiniDump格式。这个是windows提供的函数,因此必须包含“#include ”,也就是说这个方法没有办法在其他平台如linux上使用。
  2. 如果所开发的电脑软件需要在Windows、Mac OS、Linux三个平台上使用呢?
    Qt所开发的应用程序本来是跨平台的(一次编码多平台编译),那么Qt自己有没有提供类似的功能呢?答案是没有。但是这个需求确实存在,因此肯定有解决方案,那就是BreakPad。Google breakpad是一个跨平台的崩溃转储和分析框架和工具集合。而且BreakPad本来就是为了解决C、C++异常捕获而开发的。BreakPad支持跨平台,很容易在Qt项目中使用,使跨平台的应用实现跨平台异常捕获。

三、Google breakpad简单介绍

  1. 官网原文:
    Breakpad is a library and tool suite that allows you to distribute an application to users with compiler-provided debugging information removed, record crashes in compact “minidump” files, send them back to your server, and produce C and C++ stack traces from these minidumps. Breakpad can also write minidumps on request for programs that have not crashed.
    Breakpad可以捕获发布给用户的应用程序的崩溃,并记录软件崩溃的调试信息到“minidump”文件中。调试信息包括错误行号,报错详情,堆栈错误(stack traces)。软件崩溃时候把生成的“minidump”上传到自己的服务器上就可已方便的获取足够细致崩溃详情。Breakpad提供了简单的上传“minidump”到服务器的代码实现。
  2. BreakPad 被用于Google Chrome, Firefox, Google Picasa, Camino, Google Earth。
  3. BreakPad是开源协议下发布的项目由C++开发,目的在于捕捉各个系统平台下的C\C++开发的程序的崩溃详情。从而辅助修改bug。BreakPad支持的系统平台有:windows、linux、mac、ios、solaris、android ndk

四、Google breakpad实现原理简单了解

  1. 在不同平台下使用平台特有的函数以及方式实现异常捕获:
    Windows:通过SetUnhandledExceptionFilter()设置崩溃回掉函数
    Max OS:监听 Mach Exception Port 获取崩溃事件
    Linux:监听 SIGILL SIGSEGV 等异常信号 获取崩溃事件
  2. MiniDump文件格式
    minidump是由微软开发的崩溃记录文件格式。minidump为二进制文件,体积小。为了保持统一,breakpad在其他系统下也选择生成minidump文件。
  3. breakpad 查看崩溃详情的方式与原理
    breakpad官方的使用架构示意图如下:
    BreakPad工作原理示意图
    示意图要表达的意思如下:
  • 包含了breakpad客户端(静态库)的应用程序在发布Release版本的时候。在编译的时候选择保留调试信息。
  • 然后使用Breakpad 提供的symbol工具根据release版本程序生成“symbols文件”。“symbols文件”包括程序之前保留的调试信息。
  • 发布给用户的release版本程序发生崩溃后包含在程序内“breakpad客户端”会捕捉崩溃并生成“minidump文件”。
  • “minidump文件”被应用程序发送到你指定的服务器,或通过其他方式收到用户软件崩溃产生的“minidump文件”。
  • 收到“minidump文件”后,结合发布应用的所对应的 “symbols文件”,通过“breakpad minidump processor” 工具来转换生成文本格式的“stack trace 文件”。这个文件内的信息就是程序员可以直接阅读的崩溃堆栈详情。从而准确的找到触发崩溃的bug。

五、在Qt工程中集成Breakpad实现跨平台异常捕获

测试和举例所使用的Qt开发环境在win,macos,都是使用Qt Creater以及Qt creater 的工程文件。

  1. breakpad一般使用方法:
    breakpad提供的是源码,源码提供了configure和Makefile来编译。编译之后获取到一个静态库文件和对应的头文件。在平台的应用软件工程中添加静态库即可使用。在breakpad的源码中有各个平台的使用说明文档,其中Linux平台下的使用方法如下:
#include "client/linux/handler/exception_handler.h"
int main(int argc, char* argv[]) {
    google_breakpad::MinidumpDescriptor descriptor("/tmp"); 
    google_breakpad::ExceptionHandler eh(descriptor, NULL, dumpCallback, NULL, true, -1);
    crash();
    return 0;
}
static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded) {
    printf("Dump path: %s\n", descriptor.path());
    return succeeded;
}
void crash() {
    volatile int* a = (int*)(NULL);
    *a = 1;
}

  • 其中descriptor指定当捕获到崩溃的时候生成minidump文件的储存路径
  • 其中dumpCallback这个回掉函数是当保存完minidump文件之后通过这个回掉函数返回minidump文件的保存路径与文件文件名字
  • 其中crash()函数是是用于模拟引发应用软件崩溃
  • 其中“include "client/linux/handler/exception_handler.h”是特定平台需要包含的头文件
  1. Qt多个平台下使用breakpad的方法
    当使用qt开发跨平台软件使用breakpad的时候,可以对各个平台下使用breakpad方法做一个封装。从而给qt应用程序提供统一的调用接口。(注意:不同平台调用breakpad的函数和头文件是不同的)

    由于不同平台下不仅调用函数不同,编译方法也不尽相同。所以这个封装不是特别简单。因此使用使用gitHub上封装好的开源代码了。[buzzySmile/qBreakpad] 这个封装库支持在windows、linux、macos平台下使用breakpad。并且有demo帮助你快速使用qBreakpad。在qt工程中使用qBreakpad的方法如下:

  • 把clone好的qBreakpad源码添加到你的工程目录的“3rdparty”目录之下如下图所示:
    在这里插入图片描述
  • 在工程的.pro文件中添加如下编译参数
QT += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = testCrash
TEMPLATE = app
#config for qBreakpad
#CONFIG -= app_bundle #配置上这个参数以后 你的图形界面程序就会以命令行方式运行,
CONFIG +=  warn_on 
CONFIG += thread exceptions rtti stl
macx: LIBS += -framework AppKit
#without c++11 & AppKit library compiler can't solve address for symbols
CONFIG += c++11
#link qBreakpad library
include($$PWD/3rdparty/qBreakpad/qBreakpad.pri)
#end of config for qBreakpad
  • 在main.cpp中添加如下代码:
#include "mainwindow.h"
#include 
#include "QBreakpadHandler.h"
void crash() { volatile int* a = (int*)(NULL); *a = 1; }
int main(int argc, char *argv[])
{
	QApplication a(argc, argv);
	QBreakpadInstance.setDumpPath(QLatin1String("crashes"));
	MainWindow w;
	w.show();
	crash();
	return a.exec();
}
  • 这个时候编译工程会报错:error: library not found for -lqBreakpad
    是因为qBreakpad还是源代码。还没有编译生成静态库。编译qBreakpad生成对应的静态库即可正常使用qBreakpad的崩溃捕获了,在下面的章节单独讲编译。
  • 注意qbreakpad是支持上传崩溃文件到指定服务器的,可以在我提供的源码包里面的3rdparty/qBreakpad/handler/QBreakpadHandler.h找到这文件,这个部分是包括在编译生成的qBreakpad.a或qBreakpad.lib 库中的,工程包括了库和头文件之后,按照头文件定义直接调用即可。
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;
};
  • 在qBreakpad的源码中有使用demo。其中的捕获崩溃并弹出上报对话框的可实现指定上传接口网址的Qt工程非常好。 对于实现崩溃弹出对话框让用户选择是否上报非常实用(此工程在win,macos,linux通用,体现了qt的一次编码多平台编译的优势)。在提供工程例子中的3rdparty/qBreakpad/demo/reporter/目录下可以找到。其中的reporter.cpp文件如下:
#include "reporter.h"
#include "ui_reporter.h"

#include 
#include 
#include "QBreakpadHandler.h"
#include "../program/TestThread.h"
int main (int argc, char *argv[])
{
    QApplication app (argc, argv);

    QCoreApplication::setApplicationName("ReporterExample");
    QCoreApplication::setApplicationVersion("0.0.1");
    QCoreApplication::setOrganizationName("OrgName");
    QCoreApplication::setOrganizationDomain("name.org");

    // Set directory to store dumps and url to upload
    QBreakpadInstance.setDumpPath("crashes");
    // Set server type for uploading
#if defined(SOCORRO)
    QBreakpadInstance.setUploadUrl(QUrl("http://[your.site.com]/submit"));
#elif defined(CALIPER)
    QBreakpadInstance.setUploadUrl(QUrl("http://[your.site.com]/crash_upload"));
#endif

    // Create the dialog and show it
    ReporterExample example;
    example.show();

    // Run the app
    return app.exec();
}

ReporterExample::ReporterExample (QWidget *parent) :
    QDialog (parent),
    ui (new Ui::ReporterExample)
{
    // Create and configure the user interface
    ui->setupUi (this);
    this->setWindowTitle("ReporterExample (qBreakpad v."+QBreakpadHandler::version()+")");
    ui->urlLineEdit->setText(QBreakpadInstance.uploadUrl());

    ui->dumpFilesTextEdit->appendPlainText(QBreakpadInstance.dumpFileList().join("\n"));

    // Force crash app when the close button is clicked
    connect (ui->crashButton, SIGNAL (clicked()),
             this,              SLOT (crash()));

    // upload dumps when the updates button is clicked
    connect (ui->uploadButton, SIGNAL (clicked()),
             this,               SLOT (uploadDumps()));
}

ReporterExample::~ReporterExample()
{
    delete ui;
}

void ReporterExample::crash()
{
    qsrand(QDateTime::currentDateTime().toTime_t());
    TestThread t1(false, qrand());
    TestThread t2(true, qrand());

    t1.start();
    t2.start();

    QTimer::singleShot(3000, qApp, SLOT(quit()));
}

void ReporterExample::uploadDumps()
{
    QBreakpadInstance.sendDumps();
}

六、qBreakpad的编译方法

之所以把qBreakpad的编译单独拿出来讲,是因为编译要有好多注意事项,编译比较复杂。单独讲比较清晰明了。qBreakpad的源码中需要包含Breakpad的源码。

  1. 源码下载
  • qBreakpad这个封装库的开源代码下载地址,clone 下来即可。
git clone --recursive https://github.com/buzzySmile/qBreakpad.git

Clone之后,在源代码的“third_party”目录下需要下载“breakpad”的源代码和lss的源代码。其这样设计保证了封装的“breakpad”是最新版本的,不便之处就是还需要自己去下载“breakpad”的源代码。

  • Breakpad的开源代码本来是在googlesource 但是github也有镜像,地址如下:
git clone https://github.com/google/breakpad
  • 需要下载的另一个源代码是linux-syscall-support,下载好后把文件夹名字改为 “lss”:
git clone https://github.com/ithaibo/linux-syscall-support
  1. 源码编译生成静态库
  • 在MacOS下编译:在命令行切换到qBreakpad目录下,执行一下命令:“qmake”就会生成”Makefile”,然后执行“make”,等待编译完成,即可编译出“libqBreakpad.a”库文件。或者用Qt Creater 开“third_party/qBreakpad/qBreakpad.pro”这个文件,并进行编译即可,只不过编译得到的“.a”文件在qBreakpad并行以“build开头”的目录里面。需要手工放置“libqBreakpad.a”到“third_party/qBreakpad/handler”,主程序就能找到这个库。
  • 在Windows下用MinGW编译:使用mingw在windows下编译可以可以选择用命令行编译(需要配置qmake和mingw32-make的环境变量)。也可以使用QtCreater 按照编译MacOS的下使用QtCreater的步骤编译。注意用mingw在windows下生成的静态库也是以“.a”结尾。但是这个“.a”库和MacOs或Linux下的库是不同的,无法混用。
  • 在Windows下用MSVC编译:用Qt Creater 首先打开“third_party/qBreakpad/qBreakpad.pro” 工程,切换到release下编译,在qBreakpad目录并行以“build开头”的目录里面的Handler目录下可以找到编译好的“libqBreakpad.lib”文件。把这个文件复制到“third_party/qBreakpad/handler”即可。然后用Qt create 编译你调用这个库的工程。调用库的方法参考上面的“Qt多个平台下使用breakpad的方法”。注意需要把工程在Qt creater 的左下角设置为release。然后可以成功编译。这里多次提到用release,是因为在debug下,无论是“libqBreakpad.lib”还是调用他的工程都没法正常编译。报错“MD_DynamicRelease”不匹配值“MDd_DynamicDebug”,没有找到解决办法。还好release 下可用,不影响发布应用程序。
  • 没有写在Linux下怎样编译。不过编译过程应该和macos或windows下用mingw一样。

七、各平台下的例子工程下载地址

为了方便测试使用,我把我编译好的并测试可以使用的各个平台都打包好了,下 载下来可以直接使用。包括编译生成的各个编译过程文件,因此包比较大。工程文件都是Qt Creater的工程文件。为了这个包能长久稳定的提供下载,我把他上传到csdn的“资源下载”中了,系统自己生成了要5C币才能下载,造成的不方便还请谅解,或者使用这个网盘链接:
链接:https://pan.baidu.com/s/1V_8ReIHNXqQUhG2RfH79hg 密码:ylsq
Csdn下载链接:https://download.csdn.net/download/robert_cysy/11240125

八、生成symbols文件与生成Stack Trace的方法

上面讲的是怎样在自己软件里加入qBreakpad的生成dump文件的客户端。
这里讲一下软件崩溃生成minidump文件之后,怎样从这个文件通过breakpad提供的工具生成Stack Trace的方法。

从上方的BreakPad工作原理示意图可以看出,要想获得stack track需要图示的几个步骤。其中用到的几个工具是:
symbol dumper: 对应到实际命令行工具是:dump_syms
minidump processor:对应到实际命令行工具是:minidump_stackwalk

这两个命令行工具是由breakpad以源码形式提供的,需要使用者在各个平台下编译生成可执行文件,然后才能使用。

8.1编译“dump_syms”

在下载好第七节提到的工程文件的以下目录可以找breakpad的源码:
“crashtest/third_party/qBreakpad/third_party/breakpad“

  1. Linux:
    切换到breakpad所在的目录,然后依次执行以下两个命令:
    ./configure  
    Make		
    
    即可在breakpad/src/tools/linux/dump_syms/目录生成“dump_syms”可执行文件。
  2. MacOS:
    使用Finder打开目录⁨breakpad⁩ ▸ ⁨src⁩ ▸ ⁨tools⁩ ▸ ⁨mac⁩ ▸ ⁨dump_syms⁩然后可以看到编译“dump_syms”的Xcode工程文件。用Xcode打开编译即可得到“dump_syms”可执行文件。
  3. Windows:未测试

8.2编译“minidump_stackwalk”

  1. Linux,MacOS: 切换到breakpad所在的目录,然后依次执行以下两个命令:
    ./configure
    Make
    
    然后在breakpad/src/processor/目录找到“minidump_stackwalk”可执行文件.
  2. Windows:未测试

8.3使用“dump_syms”生成symbols文件

  1. Linux,MacOS:
    把测试程序test和生成的可执行文件“dump_syms”放到同一目录下,然后执行以下命令生成symbols文件,生成的文件名字指定为:“test.sym”;
    ./dump\_syms ./test > test.sym
    
  2. Windows:未测试

8.4使用“minidump_stackwalk”生成Stack Trace文件

  1. Linux,MacOS:
    把程序test崩溃生成的xxx.dmp 和test程序对应的test.sym
    放到工具“minidump_stackwalk”所在的目录,然后执行以下程序。
head -n1  test.sym

得到test.sym这个文本文件的第一行,如下
MODULE mac x86_64 887D1A2C356F3401ABCCA76B666B3A810 test
然后执行命令:

mkdir -p ./symbols/test/887D1A2C356F3401ABCCA76B666B3A810
mv test.sym ./symbols/LedStripEditor/887D1A2C356F3401ABCCA76B666B3A810
./minidump_stackwalk  xxx.dmp  ./symbols > result.txt 2> process.txt

必须严格按照上方命令执行,比如你的应用名称叫做“test”则命令中用到“test”地方名字必须都是这个名字。否则也会生成Stack Trace但是里面只是内存地址,直接看不出出错的地方。只有按照上面的步骤才能正确加载test.sym文件,从而直观的显示出崩溃详情,包括出错的函数或变量名字以及行号。以上命令把“Stack Trace”记录在result.txt文件中。把处理过程记录在文件process.txt中。
如果不设置分成">"和“2>”分别输入到两个文件,则输入部分都会输出到一个文件中,即标准输出。而实际需要看的只有result.txt的部分。为了stack trace文件清晰明了。建议这两个文件单独输出。或标准错误输出到null:“2> /dev/null”。

  1. Windows:未测试

8.5把以上命令做成脚本

以上命令会经常执行,做成脚本不仅也可以防止忘记命令操作的每一个步骤,而且极大的加快了多个命令依次执行的操作速度,脚本文件如下:

#!/bin/bash

if [ $# != 2 ] ; then 
echo "USAGE: $0 EXE_NAME DMP_NAME" 
echo " e.g.: $0 test 3872B2CF-983B-4963-AFA9-C8534DFD4C44.dmp" 
exit 1; 
fi 

#get input param
exe_file_name=$1
dmp_file_name=$2

getSymbol() {
    echo "@getSymbol: start get symbol"
    ./dump\_syms ./$exe_file_name > $exe_file_name'.sym'
}

getStackTrace() {
    echo "@getStackTrace: start get StackTrace"
    sym_file_name=$exe_file_name'.sym'

    #get first line of $sym_file_name
    line1=`head -n1 $sym_file_name`
    #echo $line1

    #get version number from string of first line
    OIFS=$IFS; IFS=" "; set -- $line1; aa=$1;bb=$2;cc=$3;dd=$4; IFS=$OIFS 
    #echo $dd
    version_number=$dd

    #make standard dir and move *.sym in it
    mkdir -p ./symbols/$exe_file_name/$version_number
    mv $sym_file_name ./symbols/$exe_file_name/$version_number

    #print stack trace at std output
    ./minidump_stackwalk $dmp_file_name ./symbols 2> /dev/null 

    #print stack trace at a file
    #./minidump_stackwalk $dmp_file_name ./symbols 2>/dev/null >result.txt
}

main() {
    getSymbol 
    if [ $? == 0 ] 
    then 
        getStackTrace
    fi
}
# run main
main

把以上脚本保存成文件“dump_tool.sh” 并通过命令"chmod +x ./dump_tool.sh"增加可执行权限。这个脚本的执行环境如下图所示:
在这里插入图片描述

九、实现Release版本程序包含调试信息

在使用breakpad的时候,需要目标应用程序内包含调试信息,这样“dump_syms”工具才能从你的应用程序中解析出调试符号。因此当你编译release版本的时候需要在项目的工程文件".pro" 文件末尾配置如下qmake参数:

#加入调试信息
QMAKE_CFLAGS_RELEASE += -g
QMAKE_CXXFLAGS_RELEASE += -g
#禁止优化
QMAKE_CFLAGS_RELEASE -= -O2
QMAKE_CXXFLAGS_RELEASE -= -O2
#release在最后link时默认有"-s”参数,表示"Omit all symbol information from the output file",因此要去掉该参数
QMAKE_LFLAGS_RELEASE = -mthreads -Wl   #此行经过测试可用可不用
#参考自:https://blog.csdn.net/dgj8300/article/details/78450638 

注意:release版最终发布给用户的时候,release版的可执行软件仍然需要在以上编译条件下编译。即最终发布给用户的可执行文件要包含调试符号。否则生成的dmp文件不能正确用“minidump_stackwalk”解析转换。

你可能感兴趣的