在c++开发中,很容易出现程序Crash而不出现任何提示的情况,尤其是在使用原生c++开发的情况,如果用了QT之类的框架还可能会输出一点点线索(也可能没有),在引入dump之前,我们都是用断点的方式debug,但是一旦发布出去,那就只能打印整个流程的所有log,来“云”调试。

所以为了解决收集和调试发布之后的程序,需要收集程序崩溃之后的信息,微软在其Windows平台上提供了minidump,需要引用WinDbg的库,注册handler也可以生成,但是这一套方案不是特别好用,而且也不能跨平台,所以Breakpad是比较好的第二方案。

Breakpad

Breakpad是谷歌开源的一个跨平台崩溃处理框架,内含崩溃转储、上报、分析一套工作流程框架。

主要的工作流程为:client以library的方式嵌入自己的程序,并设置handler,将会在程序崩溃时将会把一系列的线程列表、调用堆栈和一些系统信息写入minidump文件,再使用dump_syms将编译器生成的调试信息文件生成符号文件,最好在使用minidump_walker生成可以阅读的stack trace。

minidump

Minidump小内存转储文件是微软制定的一个应用程序崩溃后的dump文件标准,由于生成出来的文件抛弃了一些信息,所以生成的文件小,可以很轻松的通过互联网传输到服务器上,可以更有效的收集程序崩溃信息。同时使用minidump还需要配合程序的符号文件才能更好的分析进程。

Mac下使用

首先clone项目,可以从google源或者github源下载

编译Breakpad

cd breakpad
./configure && make

这一步将会编译src为一个静态链接库,当然这一步给qmake做也可以

  • processor 目录包含minidump的处理代码,不需要包含在客户端中,在需要进行分析的工作机器上使用
  • client 目录包含所有平台的breakpad头文件
  • tools 包含各种处理工具及源码

编译Breakpad tools

Breakpad自带的工具区分各个平台有不同的实现,所以编译完Breakpad主体lib只后还需要手动编译tool.

进入tool下手动编译tool

xcodebuild -project XXX.xcodeproj -configuration Release

编译完可执行文件就会出现在build/Release目录下

在QT项目中使用Breakpad

如果直接使用Breakpad的静态库,那么只需要在pro文件里加入LIB和INCLUDEPATH就可以使用。

但是我们也可以自定义Breakpad组件作为项目的一部分。

创建breakpad.pro文件

BREAKPAD_PATH=$$PWD/breakpad
INCLUDEPATH += $$BREAKPAD_PATH/src
DESTDIR = ../bin
mac: {
HEADERS += $$BREAKPAD_PATH/src/client/mac/handler/exception_handler.h
HEADERS += $$BREAKPAD_PATH/src/client/mac/crash_generation/crash_generation_client.h
HEADERS += $$BREAKPAD_PATH/src/client/mac/crash_generation/crash_generation_server.h
HEADERS += $$BREAKPAD_PATH/src/client/mac/crash_generation/client_info.h
HEADERS += $$BREAKPAD_PATH/src/client/mac/handler/minidump_generator.h
HEADERS += $$BREAKPAD_PATH/src/client/mac/handler/dynamic_images.h
HEADERS += $$BREAKPAD_PATH/src/client/mac/handler/breakpad_nlist_64.h
HEADERS += $$BREAKPAD_PATH/src/client/mac/handler/mach_vm_compat.h
HEADERS += $$BREAKPAD_PATH/src/client/minidump_file_writer.h
HEADERS += $$BREAKPAD_PATH/src/client/minidump_file_writer-inl.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/macho_utilities.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/byteswap.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/MachIPC.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/scoped_task_suspend-inl.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/file_id.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/macho_id.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/macho_walker.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/macho_utilities.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/bootstrap_compat.h
HEADERS += $$BREAKPAD_PATH/src/common/mac/string_utilities.h
HEADERS += $$BREAKPAD_PATH/src/common/linux/linux_libc_support.h
HEADERS += $$BREAKPAD_PATH/src/common/string_conversion.h
HEADERS += $$BREAKPAD_PATH/src/common/md5.h
HEADERS += $$BREAKPAD_PATH/src/common/using_std_string.h
HEADERS += $$BREAKPAD_PATH/src/common/convert_UTF.h
HEADERS += $$BREAKPAD_PATH/src/google_breakpad/common/minidump_exception_mac.h
HEADERS += $$BREAKPAD_PATH/src/google_breakpad/common/breakpad_types.h
HEADERS += $$BREAKPAD_PATH/src/google_breakpad/common/minidump_format.h
HEADERS += $$BREAKPAD_PATH/src/google_breakpad/common/minidump_size.h

SOURCES += $$BREAKPAD_PATH/src/client/mac/handler/exception_handler.cc
SOURCES += $$BREAKPAD_PATH/src/client/mac/crash_generation/crash_generation_client.cc
SOURCES += $$BREAKPAD_PATH/src/client/mac/crash_generation/crash_generation_server.cc
SOURCES += $$BREAKPAD_PATH/src/client/mac/handler/minidump_generator.cc
SOURCES += $$BREAKPAD_PATH/src/client/mac/handler/dynamic_images.cc
SOURCES += $$BREAKPAD_PATH/src/client/mac/handler/breakpad_nlist_64.cc
SOURCES += $$BREAKPAD_PATH/src/client/minidump_file_writer.cc
SOURCES += $$BREAKPAD_PATH/src/common/mac/macho_id.cc
SOURCES += $$BREAKPAD_PATH/src/common/mac/macho_walker.cc
SOURCES += $$BREAKPAD_PATH/src/common/mac/macho_utilities.cc
SOURCES += $$BREAKPAD_PATH/src/common/mac/string_utilities.cc
SOURCES += $$BREAKPAD_PATH/src/common/mac/file_id.cc
SOURCES += $$BREAKPAD_PATH/src/common/mac/MachIPC.mm
SOURCES += $$BREAKPAD_PATH/src/common/mac/bootstrap_compat.cc
SOURCES += $$BREAKPAD_PATH/src/common/md5.cc
SOURCES += $$BREAKPAD_PATH/src/common/string_conversion.cc
SOURCES += $$BREAKPAD_PATH/src/common/linux/linux_libc_support.cc
SOURCES += $$BREAKPAD_PATH/src/common/convert_UTF.cc
LIBS += /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
LIBS += /System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices
QMAKE_CXXFLAGS+=-g
}

TARGET = breakpad
CONFIG += warn_on
TEMPLATE = lib

Mac OSX 下需要额外引入两个系统库

注册handler

在main前写一个用来回调的函数,用于程序崩溃时breakpad执行自定义的内容,比如说写个log啥的,当然不用也可以

#include <QtGlobal>
#ifdef Q_OS_WIN
   #include <client/windows/handler/exception_handler.h>
#else
   #include <client/mac/handler/exception_handler.h>
#endif

#ifdef Q_OS_WIN
bool minidumpCB(const wchar_t* dump_path, const wchar_t* id, void* context, EXCEPTION_POINTERS* exinfo, MDRawAssertionInfo* assertion, bool succeeded)
{
#else
bool minidumpCB(const char* dump_path, const char* id, void* context, bool succeeded)
{
#endif
   if(succeeded)
   {
       std::wcout << "Mini Dump file: " << id << ".dump Path: " << dump_path << std::endl;
   }
   else
   {
       std::wcout << "failed to dump"<< std::endl;
   }
   return succeeded;
}

注册handler

(Mac版注释)
// Creates a new ExceptionHandler instance to handle writing minidumps.
// 创建一个ExceptionHandler去处理写入minidump
// Minidump files will be written to dump_path, and the optional callback
// is called after writing the dump file, as described above.
// minidump文件将会写入指定的dump_path目录下,且会在写入完成之后调用回调函数
// If install_handler is true, then a minidump will be written whenever
// an unhandled exception occurs.  If it is false, minidumps will only
// be written when WriteMinidump is called.
// 如果install_handler是true,minidump会在发生未处理的的exception(异常)时写入。否则只会在被调用的情况写入
// If port_name is non-NULL, attempt to perform out-of-process dump generation
// prot_name为非NULL,将会尝试执行进程外dump文件生成
// If port_name is NULL, in-process dump generation will be used.
// 为NULL,则使用进程内dump文件生成

#ifdef QT_NO_DEBUG  
#ifdef Q_OS_WIN
   google_breakpad::ExceptionHandler eh(dumpLocation.toStdWString(), NULL, minidumpCB, NULL, google_breakpad::ExceptionHandler::HANDLER_ALL);
#else
   google_breakpad::ExceptionHandler eh(dumpLocation.toStdString(), NULL, minidumpCB, NULL, true, NULL);
#endif
#endif 

到现在以及注册handler成功,可以尝试crash自己的应用程序。比如在main中写上

int* a = nullptr;
*a = 1;

程序成功的crash并且生成了minidump文件

Mini Dump file: 64975436-F039-4DB2-8A90-C7D66D7F4509.dump Path: /Users/Project/C/
  • 不过要注意在调试minidump的时候需要用到程序的symbol(符号文件)所以qt中需要选择profile模式编译,qt会在build目录生成一个.dSYM文件

使用分析工具

提取符号文件

.dSYM是一个二进制文件,所以还不能直接用需要使用dump_syms将二进制符号文件转换为text-format symbol files(文本格式符号文件)

breakpad/src/tools/mac/dump_syms/dump_syms ./test > test.sym`

储存符号文件

为了让stack trace能让正常人看得懂,还需要把文本格式的符号文件.sym放到指定的目录下面

# head -n1 test.sym

MODULE mac x86_64 5A8CD7BF25A537BB9AFF54AE8CD140200 test

所以需要新建一个目录为symbols/test/5A8CD7BF25A537BB9AFF54AE8CD140200,再把sym文件放进去

生成stack trace文件

现在就可以生成一个正常人能看得懂的Stack trace文件了

breakpad/src/processor/minidump_stackwalk 64975436-F039-4DB2-8A90-C7D66D7F4509.dump ./symbols > test.log

image.png

可以看到Crash在Thread 0,且是在main.cpp的43行,偏移0个字符长的代码发生了崩溃 image.png

跟实际的代码情况一致,这样就可以在以后程序出现崩溃的情况比较容易发现问题所在。如果程序内包含了几个库,看情况需要生成symbol文件。