转载

客户端使用breakpad收集crash

主要讲解使用如何在客户端侧使用breakpad收集crash数据,当然还有定制breakpad。填之前collect_crash的坑

how

发生crash的时候,linux的流程

在linux中,当native发生crash的时候,我们可以通过注册signal来捕获对应的signal,函数原型如下:

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

下面说一下参数的意义:

  • signum:表示signal的类别,比如,SIGSEGV、SIGABRT等等,但是不包含SIGKILL 和 SIGSTOP,我们一般捕获的是以下6个:SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, SIGTRAP

  • sigaction *act:首先介绍一下sigaction是一个结构体,其中比较关键的就是 sa_sigaction和sa_flags,/ sa_sigaction作为回调,而如果需要回调起作用,则需要设置sa_flags,通常的做法都是

    struct sigaction action{};
    action.sa_sigaction = SignalHandler;
    action.sa_flags = SA_SIGINFO | SA_ONSTACK;
    
  • Sigaction *oldact:由于每个信息只允许存在一个处理的函数,因此当我们设置我们的处理函数时会覆盖原来的处理函数,因此需要将原来的处理函数保存下来,然后当我们的函数执行完之后,再处理执行原先的处理函数。

如此设置之后,当有signal出现的时候就会回调到SignalHandler中,而这个的函数原型如下:

void SignalHandler(int sig, siginfo_t *info, void *ucontext)

下面分别介绍一下参数:

  • sig:表示的是哪个signal,参考上面的signum

  • *info:是一个结构体指针,先介绍一下siginfo_t这个结构体

    __SIGINFO struct { int si_signo; int si_errno; int si_code; union __sifields _sifields; /
    }
    

其中si_signo与sig一致,si_errno的值一般是0,si_code指示为什么这个signal会发送,__sifields一般不关心。

然后我们在SignalHandler中处理*oldact就完成了整个流程。

breakpad流程

首先放一张表示流程的自然语言:

//   SignalHandler (uses a global stack of ExceptionHandler objects to find
//        |         one to handle the signal. If the first rejects it, try
//        |         the second etc...)
//        V
//   HandleSignal ----------------------------| (clones a new process which
//        |                                   |  shares an address space with
//   (wait for cloned                         |  the crashed process. This
//     process)                               |  allows us to ptrace the crashed
//        |                                   |  process)
//        V                                   V
//   (set signal handler to             ThreadEntry (static function to bounce
//    SIG_DFL and rethrow,                    |      back into the object)
//    killing the crashed                     |
//    process)                                V
//                                          DoDump  (writes minidump)
//                                            |
//                                            V
//                                         sys_exit
//

上述的流程就是breakpad处理signal的流程,我们主要看一下DoDump()方法,主要做了如下事情:

  • 读取/proc/$pid/auxv⽂件
  • 读取/proc/$pid/task⽬录,读取进程所有的线程信息
  • 读取/proc/$pid/maps⽂件,获取当前进程加载的所有模块的信息,包含模块名、起始地址、模块
    size
  • 写minidump文件

如何定制化minidump

首先我们需要知道minidump文件的格式,格式的定义是在minidump_format.h中,但是有些结构并没有在代码中直接使用相应的对象,比如MDRawThreadList,按照之前解析class文件的经验,都是直接生成对应结构的对象,但是,由于是C语言可以直接操作地址,因此,可以不通过构建对象的方式来构建这个结构体,那么如何实现呢?不要急,我们先看一下写minidump文件的大致流程:

  1. 写header,一般都是这样处理的,不多说

  2. 写MDRawDirectory,默认是13个,结构如下:

    typedef uint32_t MDRVA;  /* RVA */
    
    typedef struct {
      uint32_t  data_size; //MDRawDirectory的大小
      MDRVA     rva;	//MDRawDirectory中第一个元素的偏移量或者说起始位置
    } MDLocationDescriptor;  /* MINIDUMP_LOCATION_DESCRIPTOR */
    
    typedef struct {
      uint32_t             stream_type;
      MDLocationDescriptor location;
    } MDRawDirectory;
    
  3. 写MDRawThreadList,这里就是上面说的问题了,你会发现整个breakpad中,并没有构建MDRawThreadList对象,而是通过偏移量来操作,首先是获取thread的数目,然后rva = originPosition+numsOfThread*sizeof(MDRawThread),这样就知道第一个MDRawThread的位置。

所以,修改的方式简单来说就是定义一个struct,然后将其插入到minidump文件的最后,然后按照规则解析出来。

如何在native crash的时候收集java堆栈

在breakpad的MinidumpCallback中是无法收集java 堆栈的,经过我的测试,只要涉及到 String 类型的数据,就会直接退出,比如你在收集Java堆栈的方法中,定义一个 String 类型的数据,当运行到这行代码时,就会直接退出,后面的代码不会运行,解决的方式是开启一个新线程收集,这样就需要涉及到线程的同步问题,换句话说,就是崩溃线程A依赖于收集java堆栈的线程B,线程B也依赖于线程A,于是我们就会想到使用互斥锁+条件变量的方式解决。具体的做法如下:

  1. 首先我们定义一个java方法,该方法用于收集java的堆栈。 public static void generateCrashProto(int crashId, final String path)

  2. JNI_OnLoad 的时候将该方法的 jmethodID 以及所属类的 jclass 保存为全局引用

  3. JNI_OnLoad 中使用 pthread_create 创建一个线程,定义回调 void* DumpJavaThreadInfo(void *argv)

  4. 定于 如下几个变量:

    static int tidCrash;  //crash线程id
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    static pthread_mutex_t mutex_finish = PTHREAD_MUTEX_INITIALIZER;
    static pthread_cond_t cond_finish = PTHREAD_COND_INITIALIZER;
    pthread_t ntid;  //新建线程id
    
  5. DumpJavaThreadInfo 中判断 tidCrash 是否为0,不为0则一直等待,即等待MinidumpCallback回调

    pthread_mutex_lock(&mutex);
        //当条件不满足时等待
        while (tidCrash == 0) {
            pthread_cond_wait(&cond, &mutex);
        }
    ...
    pthread_mutex_unlock(&mutex);
    SetDumpJavaFinish(); //通知crash线程,java堆栈收集完毕
    
  6. 在MinidumpCallback回调中对 tidCrash 赋值,并且发生信号给上一步阻塞的线程

    tidCrash = gettid();
    minidumpPath = const_cast<char *>(descriptor.path());
    if (ntid != NULL){
        pthread_mutex_lock(&mutex);
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        WaitDumpJava();  //等待获取java堆栈函数完成
    }
    
    void WaitDumpJava(){
        struct timeval now;
        gettimeofday(&now, NULL);
    
        struct timespec outtime;
        outtime.tv_sec = now.tv_sec + c_waitSecond;
        outtime.tv_nsec = 0;
        pthread_mutex_lock(&mutex_finish);
        pthread_cond_timedwait(&cond_finish, &mutex_finish, &outtime);
        pthread_mutex_unlock(&mutex_finish);
    }
    
    //在DumpJavaThreadInfo被调用
    static void SetDumpJavaFinish(){
        pthread_mutex_lock(&mutex_finish);
        pthread_cond_signal(&cond_finish);
        pthread_mutex_unlock(&mutex_finish);
    }
    
原文  http://sakurajiang.github.io/2020/06/23/client_collect_crash_by_breakpad/
正文到此结束
Loading...