google breakpad

说明:
本文出处:http://blog.163.com/cp7618@yeah/blog/static/7023477720128120224216/
再次基础上微调。

Google breakpad是 一个非常实用的跨平台的崩溃转储和分析模块,他支持Windows,Linux和Mac和Solaris。由于他本身跨平台,所以很大的减少我们在平台移 植时的工作,毕竟崩溃转储,每个平台下都不同,使用起来很难统一,而Google breakpad就帮我们做到了这一点,不管是哪个平台下的崩溃,都能够进行统一的分析。现在很多工程都在使用他:最著名的几个如 Chrome,Firefox,Picasa和Google Earth。另外他的License是BSD的,也就是说,我们即便是在商业软件中使用,也是合法的,哈哈,这么好的东西,我们能放过么?现在就让我们来 看看这个神奇的软件吧。

原理简介
breakpad把应用程序分成三个部分,代码,breakpad客户端和调试信息。

  1. 在build system中,通过symbol dumper用平台相关的调试信息生成平台无关的symbol文件。这样做的好处很明显,一旦平台无关了,所有平台的崩溃就可以做统一的分析了。
  2. breakpad采取进程外转储和分析崩溃的方式,他使用C/S结构,客户端用来捕获当前进程中发生的崩溃,并通知服务端崩溃发生。服务端用来响应客户端,抓取dump文件。这样做的目的是为了减少崩溃进程对dump的影响。
  3. Dump生成后转发到崩溃分析器中,这个部分可以在本地也可以在服务器上,他对Dump文件进行解析,生成可读的堆栈信息。

这就是breakpad处理dump大概的流程。

对于原理的介绍google写的已经相当好了。更多的详细信息,可以直接移步到breakpad的wiki。

安装和编译
breakpad的编译比较曲折,所以在此记录一下。

编译breakpad,请确认你的机器上装有以下的软件:
1. python 2.4.3 —-(depot_tools.zip)
请不要使用python3,会报错。另外python2中推荐这个版本,使用新的版本在编译其他google的工程时有时会报错

  1. Windows SDK 7
    如果没有这个,编译会报错。另外这个是在线安装,时间很久,最好并行做其他的事情。

  2. VS2005的补丁
    KB918559
    KB926601
    KB935225
    KB943969
    KB947315

已经安装了以上软件的童鞋,就可以开始进行下面的工作鸟

  1. 使用svn把代码checkout下来
  2. # Non-members may check out a read-only working copy anonymously over HTTP.

svn checkout http://google-breakpad.googlecode.com/svn/trunk/ google-breakpad-read-only

  1. 设置Windows SDK 7
    装过其他版本Windows SDK的童鞋,记得一定要进行这一步,SDK的安装程序,并不会帮你设置VS。
    运行开始菜单->程序->Microsoft Windows SDK v7.0->Visual Studio Registration->Windows SDK Configuration Tool,选择v7.0,点击Make Current。

  2. 为python设置环境变量
    由于breakpad使用python来生成Windows下的工程文件,所以需要将python所在目录,设置到环境变量PATH中去。

  3. 生成Windows工程文件

cd "源码目录/src/tools/gyp"

# 注意,此处不能使用全路径,不然会出错
gyp.bat --no-circular-check  "../../client/windows/breakpad_client.gyp"

记住要加上–no-circular-check,并且使用相对路径。命令成功执行后会在 yourpath/breakpad/src/client/windows/ 目录下生成sln文件。

需要注意的是,默认工程设置RuntimeLibrary 是MT,运用到实际工程中的时候需要注意这个,,,

  1. Hello World!
    编译build all,现在一般是不会报错了,如果报错,请检查是不是漏了什么步骤,特别是补丁。
    编译完成之后,运行crash_generation_app吧,这是他的测试程序,dump的默认位置保存在C:Dumps下,请注意先建立好目录,不然会无法使用。
    启动测试程序(Tests工程)之后,此时还不能抓取dump,因为这个是breakpad中的服务器端,需要再启动一个测试程序,在第二个测试程序中,我们就可以试验Client菜单中的各种崩溃了。这些崩溃都会被抓住转存到C:Dumps目录下。
    就是启动多个crash_generation_app.exe, 第一个为Server,其余为Client。

如何使用breakpad
在Windows下使用breakpad的方法很简单,只需要创建一个ExceptionHandler的类即可,大家可以在crash_generation_app这个工程中找到示例代码,也可以直接移步Wiki,上面说的也很详细。

1.进程内抓取Dump文件

进程内抓取Dump文件是最简单的breakpad的用法。使用方法很简单:

const std::wstring s_strCrashDir = L"c:\dumps";

bool
InitBreakpad()
{
    google_breakpad::ExceptionHandler *pCrashHandler =
        new google_breakpad::ExceptionHandler(s_strCrashDir,
        onExceptionFilter,
        onMinidumpDumped,
        NULL,
        google_breakpad::ExceptionHandler::HANDLER_ALL,
        MiniDumpNormal,
        NULL,
        NULL);

    if(pCrashHandler == NULL) {
        return false;
    }

    return true;
}

或者直接在定义一个ExcepionHandler 对象,就搞定,如下

ExceptionHandler* handler = new ExceptionHandler(
    L"C:\\dumps\\", 
    NULL,
    NULL,
    NULL,
    ExceptionHandler::HANDLER_ALL);

2.进程外抓取Dump文件

使用进程外抓取Dump时,需要指定服务端和客户端,在服务端中需要创建CrashGenerationServer的实例,而在客户端中则只需要创建ExceptionHandler即可。此外,如果服务端自己需要抓进程内的Dump,请将pipe的参数置为NULL。

const wchar_t s_pPipeName[] = L"\\.\pipe\breakpad\crash_handler_server";
const std::wstring s_strCrashDir = L"c:\dumps";

bool
InitBreakpad()
{
    google_breakpad::CrashGenerationServer *pCrashServer =
        new google_breakpad::CrashGenerationServer(s_pPipeName,
        NULL,
        onClientConnected,
        NULL,
        onClientDumpRequest,
        NULL,
        onClientExited,
        NULL,
        true,
        &s_strCrashDir);

    if(pCrashServer == NULL) {
        return false;
    }

    // 如果已经服务端已经启动了,此处启动会失败,
    if(!pCrashServer->Start()) {
        delete pCrashServer;
        pCrashServer = NULL;
    }

    google_breakpad::ExceptionHandler *pCrashHandler =
        new google_breakpad::ExceptionHandler(s_strCrashDir,
        onExceptionFilter,
        onMinidumpDumped,
        NULL,
        google_breakpad::ExceptionHandler::HANDLER_ALL,
        MiniDumpNormal,
        (pCrashServer == NULL) ? s_pPipeName : NULL, // 如果是服务端,则直接使用进程内dump
        NULL);

    if(pCrashHandler == NULL) {
        return false;
    }

    return true;
}

使用breakpad的时候,有两个地方需要注意:
1. 记得把breakpad的solution下的几个工程,包含到你开发的工程中,或者直接包含他们的lib。
2. 如果发现在common 下没有生成sln工程文件则用上述生成client 工程一样,同理。

common:基础功能,包含一个对GUID的封装和http上传的类,帮助类,看名字也知道。
exception_handler:用来捕获崩溃的类。内部产生一个crash_generation_client进行捕捉。
crash_generation_server:breakpad的服务端,用来在产生崩溃时抓取dump。
crash_generation_client:breakpad的客户端,用来捕获当前进程的崩溃。

  1. 在初始化breakpad之前,记得先创建好dump文件的目录,不然breakpad服务端将不能正常的写dump,这会导致breakpad客户端在崩溃时无限等待服务端dump写完的消息。

=======================================================================

看看breakpad在Windows下到底是如何实现的呢?

代码结构
在我们来看breakpad是如何实现其强大的功能之前,我们先来看一下他的代码结构吧。

Google breakpad的源代码都在src的目录下,他分为如下几个文件夹:
client:这下面包含了前台应用程序中捕捉dump的部分代码,里面按照平台分成各个子文件夹
common:前台后台都会用到的部分基础代码,字符串转换,内存读写,md5神马的
google_breakpad:breakpad中公共的头文件
processor:用于在后台处理崩溃的核心代码
testing:测试工程
third_party:第三方库
tools:一些小工具,用于处理dump文件和符号表

我们先来看Windows下前台实现的部分,也就是client文件夹下的代码。

breakpad的崩溃捕获机制

在Windows下捕获崩溃,大家很容易会想到那个捕获结构化异常的Api:SetUnhandledExceptionFilter。

breakpad中也使用了这个Api来实现的崩溃捕获,另外,breakpad还捕获了另外两种C++运行库提供的崩溃,一种是使用_set_purecall_handler捕获纯虚函数调用产生的崩溃,还有一种是使用_set_invalid_parameter_handler捕获错误的参数调用产生的崩溃。

    if (handler_types & HANDLER_EXCEPTION)  //1、
      previous_filter_ = SetUnhandledExceptionFilter(HandleException);

#if _MSC_VER >= 1400  // MSVC 2005/8
    if (handler_types & HANDLER_INVALID_PARAMETER) //2、
      previous_iph_ = _set_invalid_parameter_handler(HandleInvalidParameter);
#endif  // _MSC_VER >= 1400

    if (handler_types & HANDLER_PURECALL) //3、
      previous_pch_ = _set_purecall_handler(HandlePureVirtualCall);

另外由于C++运行库提供的崩溃回调中,并不会提供当前的线程现场和崩溃信息,所以breakpad会自己生成好这些信息,然后请求生成dump。
这里值得一说的是,在非异常崩溃处理中,breakpad获取线程现场使用的函数是RtlCaptureContext而不是GetThreadContext。
RtlCaptureContext只能捕获当前线程的现场,而GetThreadContext可以捕获任意线程的现场,只要有这个线程的句柄即可。
但是GetThreadContext有两个不好的地方:不能获取当前线程的现场;获取现场前必须先用SuspendThread暂停目标线程。
而RtlCaptureContext虽然只能获取当前线程的现场,但是调用他时可以不用暂停线程的运行。
对于breakpad来说,崩溃发生后越早获取现场就越好,所以breakpad使用RtlCaptureContext函数作为他的线程获取函数。

breakpad中的C/S结构
由于breakpad是在进程外抓取dump,所以breakpad需要实现一个C/S结构来处理崩溃进程抓取dump的请求。

  1. breakpad跨进程通信的实现
    breakpad中使用了命名管道来实现IPC。
const wchar_t kPipeName[] = L"\\\\.\\pipe\\BreakpadCrashServices\\TestServer";

在客户端,初始化ExceptionHandler的时候,如果指定了PipeName,也就表示此时需要使用进程外的dump抓取,ExceptionHandler,会建立一个 CrashGenerationClient的对象,由这个对象连接服务端,将自己注册到服务端上去。
大家可以参看exception_handler.cc中的ExceptionHandler::Initialize函数。

在服务端,初始化CrashGenerationServer的时候,就会建立一个命名管道,并等待客户端来连接。一旦有客户端连接上来,服务端会为每一个客户端生成一个ClientInfo的对象,之后用这个对象来管理所有的客户端,一旦有崩溃发生,服务端都会从这个对象中取出dump所需要的信息。
大家可以参看crash_generation_server.cc中的CrashGenerationServer::HandleReadDoneState函数。

  1. breakpad捕获崩溃生成dump的流程
    breakpad进程外生成dump的流程大概如下:
    google-breakpad-out-of-process-dump:
    google-breakpad-out-of-process-dump
    这段流程的代码就是crash_generation_client.cc和crash_generation_server.cc。

有两个简单的问题,这里说明一下,高手们就请直接忽略吧,咩哈哈:
在服务端如何为客户端生成事件句柄?
使用DuplicateHandle,即可把任意一个内核对象的句柄复制到其他进程,并且可以指定产生的句柄的权限。
举个里边的例子吧

    DuplicateHandle(client_info.process_handle(),  // hSourceProcessHandle
                    reply->dump_request_handle,    // hSourceHandle,
                    NULL,                          // hTargetProcessHandle
                    0,                             // lpTargetHandle
                    0,                             // dwDesiredAccess
                    FALSE,                         // bInheritHandle
                    DUPLICATE_CLOSE_SOURCE);       // dwOptions

///, 通知源句柄所在进程,目标进程需要访问 这个资源句柄,,,;嘿嘿,别人的东西需要通知下对方,否则就是偷盗嘛
  if (!DuplicateHandle(current_process,
                       client_info.dump_requested_handle(), ///< 即要复制的句柄
                       client_info.process_handle(),        ///< 承接所在进程句柄
                       &reply->dump_request_handle,         ///< 复制承接
                       kDumpRequestEventAccess,
                       FALSE,
                       0))

知道了这个,加上pipe的知识就差不多了知道了个大概,其他的是细节问题。

如何异步的等待一个事件?
使用RegisterWaitForSingleObject,即可异步的等待一个事件,当事件发生的时候,就可以回调到一个指定的回调函数中,但是要注意的是,RegisterWaitForSingleObject会在一个新的线程中来等待这个事件,此处很容易产生多线程的调用,需要注意线程问题。

  1. 服务端关键数据结构:ClientInfo
    ClientInfo是服务端中最重要的数据结构,服务端通过它来管理所有的客户端。客户端注册时,会保存或生成里面所有的信息,在客户端请求生成dump的时候,服务端就会通过ClientInfo获取所有客户端的信息。ClientInfo中保存了如下信息:

客户端进程pid和句柄
生成Minidump的类型
自定义的客户端信息
客户端崩溃的线程ID
客户端崩溃的信息
客户端请求崩溃所使用的事件句柄

这里有一个问题:在客户端发生崩溃时,服务器如何通过ClientInfo获取到客户端的崩溃信息呢?

客户端中有几个用于保存崩溃信息的变量,在注册时,客户端会将这几个变量的地址发送至服务端,服务端将其保存在ClientInfo中,然后当崩溃发生的时候,服务端就可以通过ReadProcessMemory读取客户端中的信息,从而生成dump。这样做就避免了每次发生崩溃,都要通过Pipe将崩溃信息传递到服务端中去了。

这些变量分别是:崩溃的线程ID,EXCEPTION_POINTERS和MDRawAssertionInfo。
EXCEPTION_POINTERS和MDRawAssertionInfo的区别在于,异常崩溃的信息会被写入EXCEPTION_POINTERS,非异常崩溃(非法参数和纯虚函数调用)的信息会被写入MDRawAssertionInfo中。

dump文件的上传

在breakpad的工程中,有一个工程叫做:crash_report_sender,里面是一个上传崩溃文件的类,他的实现很简单,他使用Windows Internet Api来完成dump文件的上传。
在使用crash_report_sender时,可以为其指定一个checkpoint_file。

1

explicit
CrashReportSender(
const
wstring &checkpoint_file);

这个文件只有一个作用,就是用来保存上次上传崩溃的时间和今天上传过的崩溃的次数。通过这个文件,我们就可以来设置每日上传的崩溃的最大数量。

static const char kCheckpointSignature[] = "GBP1\n";

CrashReportSender::CrashReportSender(const wstring &checkpoint_file)
    : checkpoint_file_(checkpoint_file),
      max_reports_per_day_(-1),
      last_sent_date_(-1),
      reports_sent_(0) {
  FILE *fd;
  if (OpenCheckpointFile(L"r", &fd) == 0) {
    ReadCheckpoint(fd);
    fclose(fd);
  }
}

调整每日上传崩溃的最大数量的函数是set_max_reports_per_day。

需要注意的是:在上传dump文件的时候,crash_report_sender并不会对dump文件进行分析,而是直接上传整个dump文件,如果你需要上传的dump文件非常大的话,可以考虑把崩溃分析处理的逻辑放入前台,通过去重或者直接上传分析结果,减少上传的文件大小。

breakpad存在的问题
进程外生成dump有很多好处,其中最大的好处就是不会被崩溃进程影响,这样dump的过程就不容易出错,但是这样也有一定的弊端。

  1. 部分崩溃无法抓取
    在一些极端的崩溃,如堆栈溢出之类的崩溃,进程外抓取dump有时候会失败。

  2. 无法抓取死锁或者其他原因导致的进程僵死
    breakpad现在没有检测进程死锁的代码,也没有在服务端控制客户端请求dump的代码,所以现在breakpad无法抓取死锁等进程僵死的问题。不过因为breakpad的定位是处理崩溃,如果有这种需要的童鞋,可以自行修改breakpad的代码,添加这些功能。

  3. 对服务端有依赖
    如果指定了在使用进程外抓取dump,breakpad对服务端就有依赖。主要体现在抓取dump时,如果服务端不存在,客户端将无法正常抓取dump,甚至有时会出现阻塞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值