what
主要讲解使用如何在客户端侧使用breakpad收集crash数据,当然还有定制breakpad。填之前collect_crash的坑
how
发生crash的时候,linux的流程
在linux中,当native发生crash的时候,我们可以通过注册signal来捕获对应的signal,函数原型如下:
1 | int sigaction(int signum, const struct sigaction *act, |
下面说一下参数的意义:
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
3struct 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 | // SignalHandler (uses a global stack of ExceptionHandler objects to find |
上述的流程就是breakpad处理signal的流程,我们主要看一下DoDump()方法,主要做了如下事情:
- 读取/proc/$pid/auxv⽂件
- 读取/proc/$pid/task⽬录,读取进程所有的线程信息
- 读取/proc/$pid/maps⽂件,获取当前进程加载的所有模块的信息,包含模块名、起始地址、模块
size - 写minidump文件
如何定制化minidump
首先我们需要知道minidump文件的格式,格式的定义是在minidump_format.h中,但是有些结构并没有在代码中直接使用相应的对象,比如MDRawThreadList,按照之前解析class文件的经验,都是直接生成对应结构的对象,但是,由于是C语言可以直接操作地址,因此,可以不通过构建对象的方式来构建这个结构体,那么如何实现呢?不要急,我们先看一下写minidump文件的大致流程:
写header,一般都是这样处理的,不多说
写MDRawDirectory,默认是13个,结构如下:
1
2
3
4
5
6
7
8
9
10
11typedef 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;写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,于是我们就会想到使用互斥锁+条件变量的方式解决。具体的做法如下:
首先我们定义一个java方法,该方法用于收集java的堆栈。
public static void generateCrashProto(int crashId, final String path)
在
JNI_OnLoad
的时候将该方法的jmethodID
以及所属类的jclass
保存为全局引用在
JNI_OnLoad
中使用pthread_create
创建一个线程,定义回调void* DumpJavaThreadInfo(void *argv)
定于 如下几个变量:
1
2
3
4
5
6static 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在
DumpJavaThreadInfo
中判断tidCrash
是否为0,不为0则一直等待,即等待MinidumpCallback回调1
2
3
4
5
6
7
8pthread_mutex_lock(&mutex);
//当条件不满足时等待
while (tidCrash == 0) {
pthread_cond_wait(&cond, &mutex);
}
...
pthread_mutex_unlock(&mutex);
SetDumpJavaFinish(); //通知crash线程,java堆栈收集完毕在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
27tidCrash = 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);
}