what

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

how

发生crash的时候,linux的流程

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

1
2
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,通常的做法都是

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

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

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

下面分别介绍一下参数:

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

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

    1
    2
    __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流程

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//   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个,结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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. 定于 如下几个变量:

    1
    2
    3
    4
    5
    6
    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回调

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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);
    }