google breakpad for linux(1)

写代码的人最难堪而又无法回避的事情之一,莫过于你写的程序某刻当着 QA 美女的面突然挂掉 --- 大大没面子!但更没面子的是,之后你一直没法解决问题。。。程序崩溃而又无法解决可能有很多的原因,其中一个就是无法找到出问题的地方,尤其是那些 release 版本的程序。异常崩溃后的善后处理是一件很重要而又不大好做的事情,一方面事关用户体验,另一方面能否尽可能收集崩溃现场的信息关系着接下来能否快速和及时有效地解决问题,google breakpad 可以说算是专门为解决这类事情而开发出来的工具。简单来说,它的主要作用是在程序崩溃后,输出一个特殊的 coredump,并且然后根据 coredump 还原崩溃时函数的调用栈,为了做到这些它提供了一些手段,使得你的程序能够捕获异常,从而能让它在弥留之际可以煽情地留下些什么,而不是突然就凭空消失了。

具体的介绍读者可以参考一下官方的说明:https://code.google.com/p/google-breakpad/wiki/GettingStartedWithBreakpad. 本文只简单记一下怎样在我们的代码中使用该工具,后续会探讨一下实现的细节。

使用流程:

a. 代码中加入 breakpad 模块(后面介绍)。

b. 加 -g 编译程序,然后用 breakpad 提供的工具 dump_syms 从编译好的可执行文件中 dump 出一个符号文件,dump 的时候需指定相应的程序,及最后输出的符号文件的名字,如下:    

    dump_syms ./test > test.sym

  如果你的程序需要链接其它的动态链接库(比如libc.so,其它第三方或自己的动态库),则最好也同样把这些动态库的符号dump下来,程序崩溃函数的调用链穿过动态库这属于太正常的现象了,如果没有这些相应的动态库的符号,还原不出库里的函数的名字还是小事,更麻烦的是有时因为缺失这些符号信息,常常搞得调用链上其它有符号的函数也没法正确的还原出来。

 在完成dump符号后,你就可以把程序的 debug 信息 strip 掉了,这个特性可以说是与 coredump 相比最大一亮点,dump 完 symbol 后 strip 完全不影响后续的崩溃现场还原。

c. 发布程序,用户在使用程序的过程中,如果不幸发生了崩溃,breakpad 会及时产生 mini dump,我们需要把这个 mini dump 拿回来,配合前面 dump 出来的符号文件,再在 breakpad 提供的工具 minidump_stackwalk的帮助下,我们可以还原出程序崩溃那一刻的 call stack。

  minidump_stackwalk 在使用上有些麻烦,需要如下步骤:

    1.需要先把前面dump下来的符号文件改一下名字,改为符号文件中第一行的倒数第二个字符,这是一长串的16进制字符串。

    2.然后把这个符号文件移到./symbols/test/  目录下,test是你的程序的名字。

    3. 动库也需要做同样的处理,放到同一目录下或放到别的目录都可以。

   $ head -n1 test.sym
MODULE
Linux x86_64 6EDC6ACDB282125843FD59DA9C81BD830 test
$ mkdir
-p ./symbols/test/6EDC6ACDB282125843FD59DA9C81BD830
$ mv test
.sym ./symbols/test/6EDC6ACDB282125843FD59DA9C81BD830

$ head -n1 libc.sym
MODULE Linux x86_64 G4YCJAGNB282895843FD59DA9C81BD830 libc.so
$ mkdir -p ./symbols/libc.so/
G4YCJAGNB282895843FD59DA9C81BD830
$ mv libc.so.sym ./symbols/
libc.so/G4YCJAGNB282895843FD59DA9C81BD830

   4. 最后运行:

         minidump_stackwalk minidump.dmp ./symbols

上面的步聚如果过于麻烦,你可以自己修改一下dump_syms这个工具,dump的时候就把目录结构给建好了,免得手动操作,或者也可以借用一下火狐提供的一个python写的工具,一步到位:

http://mxr.mozilla.org/mozilla-central/source/toolkit/crashreporter/tools/symbolstore.py

怎样把 breakpad 加入程序中

1) 编译breakpad.

  按照说明进行编译后,会得到一个libbreakpad_client.a 及一系列的工具。

  其中最主要的两个在:  

    a. /src/tools/linux/dump_syms/dump_syms  

    b. /src/processor/minidump_stackwalk

  有一点需要提一下,这个库在低版本的gcc中编译不了,比较不方便,google code上有人提了这个问题,也有人做了些patch,但由于库在实现上依赖了一些高版本编译器的特性,作者并不打算去解决这个问题了。

2) 把 libbreakpad_client.a 链接到你的程序中.

3) 至于怎么在你的程序加以使用,其实非常简单,示例如下:     

#include "client/linux/handler/exception_handler.h"
#include "
third_party/lss/linux_syscall_support.h"
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descr,
                         void*context,bool succeeded)
{
   printf("crash occurs!\n");
   pid_t ret = sys_fork();
   if(ret < 0)
    {
        fprintf(stderr,"mini dump fork error/n");
        return false;
    }
    else if(ret == 0)
    {
        printf("child process\n");

         char* const arg[] = {"ch",(char *)descr.path(),NULL};
         char* const env[] = {NULL,NULL};

 
 

          sys_execve("./ch",arg,env);

        // execl("./dh","dh",descr.path(),0);
        printf("error , we should never be here\n");
    }
    else
    {

    }
    return true;
}

int main()
{
    printf("crash source.\n");

    using namespace google_breakpad;
    MinidumpDescriptor desc("./dump");
    ExceptionHandler eh(desc,NULL,dumpCallback,NULL,true,-1);

    printf("ready to crash\n");

    int num = 0;
    int div = 3;

    int i = div/num;

    return 0;
}

一点解释

现在我们来看看 ExceptionHandler 的原型。

// Creates a new ExceptionHandler instance to handle writing minidumps.                                                  
119   // Before writing a minidump, the optional |filter| callback will be called.                                             
120   // Its return value determines whether or not Breakpad should write a                                                    
121   // minidump.  The minidump content will be written to the file path or file                                              
122   // descriptor from |descriptor|, and the optional |callback| is called after                                             
123   // writing the dump file, as described above.                                                                            
124   // If install_handler is true, then a minidump will be written whenever                                                  
125   // an unhandled exception occurs.  If it is false, minidumps will only                                                   
126   // be written when WriteMinidump is called.                                                                              
127   // If |server_fd| is valid, the minidump is generated out-of-process.  If it                                             
128   // is -1, in-process generation will always be used.                                                                     
129   ExceptionHandler(const MinidumpDescriptor& descriptor,                                                                   
130                    FilterCallback filter,                                                                                  
131                    MinidumpCallback callback,                                                                              
132                    void *callback_context,                                                                                 
133                    bool install_handler,                                                                                   
134                    const int server_fd); 

可见,官方已经把功能做得相当傻瓜,使用者只要定义一个ExceptionHandler 的对象,传入几个 callback,这些 callback 就会在异常出现时被适时调用,这些 callback 主要包括:

1) FilterCallback,过滤一下,该处理哪些异常。

2) MiniCallback,在异常发生,且 mini coredump 已经被输出后,这个 callback 会被调用。

// If a FilterCallback returns true, Breakpad will continue processing,                                                  
 84   // attempting to write a minidump.  If a FilterCallback returns false,                                                   
 85   // Breakpad  will immediately report the exception as unhandled without                                                  
 86   // writing a minidump, allowing another handler the opportunity to handle it.                                            
 87   typedef bool (*FilterCallback)(void *context);                                                                           
 88                                                                                                                            
 89   // A callback function to run after the minidump has been written.                                                       
 90   // |descriptor| contains the file descriptor or file path containing the                                                 
 91   // minidump. |context| is the parameter supplied by the user as                                                          
 92   // callback_context when the handler was created.  |succeeded| indicates                                                 
 93   // whether a minidump file was successfully written.                                                                     
 94   //                                                                                                                       
 95   // If an exception occurred and the callback returns true, Breakpad will                                                 
 96   // treat the exception as fully-handled, suppressing any other handlers from                                             
 97   // being notified of the exception.  If the callback returns false, Breakpad                                             
 98   // will treat the exception as unhandled, and allow another handler to handle                                            
 99   // it. If there are no other handlers, Breakpad will report the exception to                                             
100   // the system as unhandled, allowing a debugger or native crash dialog the                                               
101   // opportunity to handle the exception.  Most callback implementations                                                   
102   // should normally return the value of |succeeded|, or when they wish to                                                 
103   // not report an exception of handled, false.  Callbacks will rarely want to                                             
104   // return true directly (unless |succeeded| is true).                                                                    
105   typedef bool (*MinidumpCallback)(const MinidumpDescriptor& descriptor,                                                   
106                                    void* context,                                                                          
107                                    bool succeeded);

 

需要注意的是,在异常出现后,崩溃的进程常常已经被破坏了,因此在这个 callback 里不要做太多事情,特别是内存访问,写入以及调用别的库的函数等,这些很难保证能正常工作。

 Note: You should do as little work as possible in the callback function. Your application is in an unsafe state. It may not be safe to allocate memory or call functions from other shared libraries.

这里特指不要调用动态链接库中的函数,原因有如下几个:

1. 有的动态库此时可能还没完全被加载完毕(如符号重定位),在这种时候调用其中的函数会触发动态动态库运行时加载的一系列动作,这在程序已经崩溃的情况下通常做不到。

2. 动态库中的函数可能会引用 heap 上某些内存,而程序崩溃时,这些内存有可能被破坏了,库中函数未必能正常工作。

3. 对某些 libc 中的函数,它们是有锁的,程序崩溃时,其它线程都会被暂停,此时如果某些线程恰好也调用了这些函数并取得了锁,那么在信号处理函数中就无法再获得锁,因此会死锁。

 

唯一的解决方法是绕过 libc 直接通过 syscall 来与 kernel 打交道,被推荐的做法是,直接在 callback 里 fork 一个子进程出来,exec 新的程序,再进行异常处理。有人也许会问,fork,exec 等常用 POSIX 接口,不就是在动态库中吗? 是的!所以我们只好把它们抛弃!但我们对它们是这样的熟悉,以至于依赖,如果完全不用这些 libc,glibc 中的函数,人生简直失去了意义。。。

 

人生可以没意义,事情还是要做的,脱离了glibc,libc 虽然难受,但还不至于活不下去。对于很多如 fork,exec 这类的系统调用来说,glibc 只是对它们的进行了一个包装,我们完全可以直接通过 syscall 一系列函数来直接与内核打交道。

唯一的问题是,凡事都要自己动手,人生没意义之外,还未免太痛苦。

这里 google 又帮了点小忙,你直接可以引入一个头文件:lss/linux_syscall_support.h, 该头文件提供了很多的直接进行系统调用的宏与函数(其实就是那一堆 syscall 的封装),做的很贴心,很多我们用到系统调用,它都包装成了类似的名字,如常用的 fork() ,它就包装了一个 sys_fork() 代替。换而言之,在所有用到 POSIX 接口中的函数地方,你全部加上 sys_ 开头,基本就能直接替换,如果不幸哪些函数没有,那就只能自己动手来 syscall 了。

 

友情提示,exec 系列函数,linux_syscall_support.h 中就只提供了一个 sys_execve,别的如 execl 等都没有定义。

 

除了这些系统调用相关的,google breakpad 也提供了一些字符串操作函数用来替代 libc 中常用诸如 strcpy,strlen 之类的函数,但是数量非常有限,这些替代函数可以在头文件: commonlinuxlinux_libc_support.h 里找到。上述代码中,exec 的程序如下:

#include<unistd.h>
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>

static void Reporter(const char* path)
{
    printf("crash reporter process.\n");

    printf("dump file path:%s\n",path);
}

int main(int arg,char* argv[])
{
  if(arg == 2)
  {
          Reporter(argv[1]);
  }
}

    

显然,这里只是简单地把 coredump 的路径打印了一下,功能完善的 crash reporter 应该在这里起一个GUI, 询问一下用户可否愿意上传 coredump 及描述一下崩溃时的相关操作信息,并处理上传的逻辑。

breakpad 还很贴心地提供了一个通过 http 来上传 coredump 的工具,在如下路径中:src/tools/linux/symupload/ 。这个工具的使用也是相当方便的,只需要搭起一个简易的 http server,在客户端设置一下用来接收coredump 的 server 的 url,及 coredump 的路径就可以直接使用。 当然,如果你的上传逻辑需要定制更多细节的话,就需要自己在它的基础加以修改了。

结语

工欲就其事,必先利其器,谷歌一系列的开源工具,让不少开发者受益匪浅,应当表扬一下。开源软件在提高大众软件开发效率和生产应用上的贡献,确实不可磨灭。只是喝水不忘挖井人才好,开源,人类的希望!

 

你可能感兴趣的:(Google)