CoffeeCatch 原理分析
coffeecatch 是一款可以用于crash捕捉的C++库
它只有两个文件,实现原理比较简单。
coffeecatch.h 和coffeecatch.c
一、coffeecatch的基本使用
它的用法类似于try catch结构,将可能会发生crash的代码 放到try{}块中,发生crash后,在catch 块中提取crash信息
extern "C"
JNIEXPORT void JNICALL
Java_com_example_testunwind2_MainActivity_go2CrashCoffeeCatch(JNIEnv *env, jobject instance) {
COFFEE_TRY(){
go2Crash4();
}COFFEE_CATCH(){
const char*const message = coffeecatch_get_message();
ALOGD("feifei----- enter COFFEE_CATCH :%s",message);
}COFFEE_END();
}
int getCrash2(){
int i = 0;
int j = 10/i;
}
void go2Crash3(){
getCrash2();
}
void go2Crash4(){
go2Crash3();
}
crash时 的堆栈输出:
2020-03-06 13:50:04.163 15876-15876/com.example.testunwind2 D/feifei_native: feifei----- enter COFFEE_CATCH :signal 5 (Process breakpoint)
[at /data/app/com.example.testunwind2-6-w04Pe1a6eBOR6xeZpnpw==/lib/arm64/libnative-lib.so:0x10770 (_Z9getCrash2v+0x18)]
[at /data/app/com.example.testunwind2-6-w04Pe1a6eBOR6xeZpnpw==/lib/arm64/libnative-lib.so:0x14ad8]
[at /data/app/com.example.testunwind2-6-w04Pe1a6eBOR6xeZpnpw==/lib/arm64/libnative-lib.so:0x1451c]
[at /data/app/com.example.testunwind2-6-w04Pe1a6eBOR6xeZpnpw==/lib/arm64/libnative-lib.so:0x14264]
[at [vdso]:0x7e66ce468c]
[at /data/app/com.example.testunwind2-6-w04Pe1a6eBOR6xeZpnpw==/lib/arm64/libnative-lib.so:0x1076c (_Z9getCrash2v+0x14)]
[at /data/app/com.example.testunwind2-6-w04Pe1a6eBOR6xeZpnpw==/lib/arm64/libnative-lib.so:0x1077c (_Z9go2Crash3v+0x8)]
[at /data/app/com.example.testunwind2-6-w04Pe1a6eBOR6xeZpnpw==/lib/arm64/libnative-lib.so:0x10790 (_Z9go2Crash4v+0x8)]
[at /data/app/com.example.testunwind2-6-w04Pe1a6eBOR6xeZpnpw==/lib/arm64/libnative-lib.so:0x10a94 (Java_com_example_testunwind2_MainActivity_go2CrashCof
二、原理分析
在coffeeCatch.h 中有这样一段宏定义。
#define COFFEE_TRY() \
if (coffeecatch_inside() || \
(coffeecatch_setup() == 0 \
&& sigsetjmp(*coffeecatch_get_ctx(), 1) == 0))
#define COFFEE_CATCH() else
#define COFFEE_END() coffeecatch_cleanup()
/** End of internal functions & definitions. **/
#ifdef __cplusplus
}
#endif
#endif
上面的try catch块实际是执行了如下操作:
if (coffeecatch_inside() || \
(coffeecatch_setup() == 0 \
&& sigsetjmp(*coffeecatch_get_ctx(), 1) == 0)){
go2Crash4();
}else{
const char*const message = coffeecatch_get_message();
ALOGD("feifei----- enter COFFEE_CATCH :%s",message);
}coffeecatch_cleanup();
coffeecatch_inside()的作用主要是判断是否已经初始化了coffeecatch的环境,第一次运行返回false,我们暂且不看。
1、coffeecatch_setup
首先看看coffeecatch_setup做了什么。
从注释中可以看到 主要是初始化了一个crash handler。为context 做了一个标记,表示已经调用过coffeecatch_handler_setup() 。
/**
* Calls coffeecatch_handler_setup(1) to setup a crash handler, mark the
* context as valid, and return 0 upon success.
*/
int coffeecatch_setup() {
if (coffeecatch_handler_setup(1) == 0) {
native_code_handler_struct *const t = coffeecatch_get();
assert(t != NULL);
assert(t->reenter == 0);
t->reenter = 1;
t->ctx_is_set = 1;
return 0;
} else {
return -1;
}
}
我们继续看 coffeecatch_handler_setup做了什么事情。
/**
* Acquire the crash handler for the current thread.
* The coffeecatch_handler_cleanup() must be called to release allocated
* resources.
**/
static int coffeecatch_handler_setup(int setup_thread) {
int code;
ALOGD("coffeecatch_handler_setup\n");
/* Initialize globals. */
if (pthread_mutex_lock(&native_code_g.mutex) != 0) {
return -1;
}
ALOGD("coffeecatch_handler_setup_global\n");
//(1) 初始化信号处理函数
code = coffeecatch_handler_setup_global();
if (pthread_mutex_unlock(&native_code_g.mutex) != 0) {
return -1;
}
/* Global initialization failed. */
if (code != 0) {
return -1;
}
/* Initialize locals. */
if (setup_thread && coffeecatch_get() == NULL) {
//(2)初始化了native_code_handler_struct 对象。
native_code_handler_struct *const t =
coffeecatch_native_code_handler_struct_init();
if (t == NULL) {
return -1;
}
ALOGD("installing thread alternative stack 2222 \n");
//(3)将native_code_handler_struct 指针保存到线程独享变量中。
/* Set thread-specific value. */
if (pthread_setspecific(native_code_thread, t) != 0) {
coffeecatch_native_code_handler_struct_free(t);
return -1;
}
ALOGD("installed thread alternative stack\n");
}
/* OK. */
return 0;
}
它主要做了两件事情:
(1)coffeecatch_handler_setup_global() 注册信号量和信号处理函数
/* Initialize globals. */
if (pthread_mutex_lock(&native_code_g.mutex) != 0) {
return -1;
}
ALOGD("coffeecatch_handler_setup_global\n");
//(1) 初始化信号处理函数
code = coffeecatch_handler_setup_global();
if (pthread_mutex_unlock(&native_code_g.mutex) != 0) {
return -1;
}
(2)创建了native_code_handler_struct结构体,然后将其保存在了线程独有Key中
coffeecatch_native_code_handler_struct_init 初始化native_code_handler_struct结构体
pthread_setspecific(native_code_thread, t) != 0
2、我们继续看信号量是如何被处理的
/* Internal globals initialization. */
static int coffeecatch_handler_setup_global(void) {
if (native_code_g.initialized++ == 0) {//保证是首次调用
size_t i;
//(1)声明两个sigaction 用于处理信号事件,sa_abort用户处理abort信号,sa_pass用于处理其他信号。
struct sigaction sa_abort;
struct sigaction sa_pass;
ALOGD("installing global signal handlers\n");
/* Setup handler structure. */
memset(&sa_abort, 0, sizeof(sa_abort));
sigemptyset(&sa_abort.sa_mask);
sa_abort.sa_sigaction = coffeecatch_signal_abort;
sa_abort.sa_flags = SA_SIGINFO | SA_ONSTACK;
//(2)注意此处的flags参数: SA_SIGINFO 指定使用sa_sigaction (而非sa_handler)作为信号处理函数,SA_ONSTACK 表示开启备用栈,信号处理函数在备用栈上运行
memset(&sa_pass, 0, sizeof(sa_pass));
sigemptyset(&sa_pass.sa_mask);
sa_pass.sa_sigaction = coffeecatch_signal_pass;
sa_pass.sa_flags = SA_SIGINFO | SA_ONSTACK;
/* Allocate */ // (3)native_code_g.sa_old 用于保存 该信号之前安装的信号处理函数.
native_code_g.sa_old = calloc(sizeof(struct sigaction), SIG_NUMBER_MAX);
if (native_code_g.sa_old == NULL) {
return -1;
}
/**
* SIGABRT, SIGILL, SIGTRAP, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT 总共支持了7种信号量,sigabrt使用coffeecatch_signal_abort函数来处理,其他使用coffeecatch_signal_pass来处理。
*/
/* Setup signal handlers for SIGABRT (Java calls abort()) and others. **/
for (i = 0; native_sig_catch[i] != 0; i++) {
const int sig = native_sig_catch[i];
const struct sigaction * const action =
sig == SIGABRT ? &sa_abort : &sa_pass;
assert(sig < SIG_NUMBER_MAX);
ALOGD("coffeecatch_handler_setup_global - install signal:%d",sig);
//(4)调用sigaction函数 为信号量安装处理函数
if (sigaction(sig, action, &native_code_g.sa_old[sig]) != 0) {
return -1;
}
}
//(5)初始化一个线程变量
/* Initialize thread var. */
if (pthread_key_create(&native_code_thread, NULL) != 0) {
return -1;
}
ALOGD("install signal handler success\n");
}
/* OK. */
return 0;
}
函数运行在用户态,当遇到系统调用、中断或是异常(包括crash)的情况时,内核会接收到对应的信号,然将其放到对应进程的信号队列中,由对应的进程的信号处理函数来处理该信号。native crash的捕捉也就是在信号处理函数完成的。
信号机制和Android natvie crash捕捉
脑补sigaction()函数和sigaction结构体
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); //默认信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); //可以发送附加信息的信号处理函数,sa_flag设置了SA_SIGINFO使用其处理
sigset_t sa_mask;//在此信号集中的信号在信号处理函数运行中会被屏蔽,函数处理完后才处理该信号
int sa_flags;//可设参数很多
void (*sa_restorer)(void);//在man手册里才发现有这玩意,还不知道啥用
};
coffeecatch中 共处理的7种信号量:
SIGABRT, SIGILL, SIGTRAP, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT
声明了两个sigaction结构体,sa_abort和sa_pass
sigabrt使用coffeecatch_signal_abort函数来处理,其他信号量使用coffeecatch_signal_pass来处理。
/* Setup handler structure. */
memset(&sa_abort, 0, sizeof(sa_abort));
sigemptyset(&sa_abort.sa_mask);
sa_abort.sa_sigaction = coffeecatch_signal_abort; //指定信号处理函数
sa_abort.sa_flags = SA_SIGINFO | SA_ONSTACK;
//(2)注意此处的flags参数: SA_SIGINFO 指定使用sa_sigaction (而非sa_handler)作为信号处理函数,SA_ONSTACK 表示开启备用栈,信号处理函数在备用栈上运行
- a_abort.sa_sigaction 捕捉到信号量之后的信号处理函数
- sa_abort.sa_flags SA_SIGINFO 指定使用sa_sigaction (而非sa_handler)作为信号处理函数
- sa_abort.sa_flags ,SA_ONSTACK 表示开启备用栈,信号处理函数在备用栈上运行,而不是运行在系统原有的栈结构上(因为发生crash时也许系统的栈已经溢出,如果继续再系统栈上运行可能会引起二次崩溃。
pthread_key_create 又是做了什么呢?
线程私有变量脑补
C 语言中有一种线程独有数据的方式,即只有当前线程中可以访问当前线程声明的变量,其他线程访问该变量得到的是一个新的值。就相当于JAVA 中的ThreadLocal线程独有变量。
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*))
第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。
int pthread_setspecific(pthread_key_t key,const void *pointer));
void *pthread_getspecific(pthread_key_t key);
set是把一个变量的地址告诉key,一般放在变量定义之后,get会把这个地址读出来,然后你自己转义成相应的类型再去操作,注意变量的有效期。
一般的处理流程如下:
1、创建一个键
2、为一个键设置线程私有数据
3、从一个键读取线程私有数据void *pthread_getspecific(pthread_key_t key);
4、线程退出(退出时,会调用destructor释放分配的缓存,参数是key所关联的数据)
5、删除一个键
由此可知 pthread_key_create 创建了一个key native_code_thread 用于维护线程私有变量
pthread_key_create(&native_code_thread, NULL)
3、 coffeecatch_native_code_handler_struct_init
我们来看coffeecatch_native_code_handler_struct_init中到底做了什么事情:
/**
* Create a native_code_handler_struct structure.
**/
static native_code_handler_struct* coffeecatch_native_code_handler_struct_init(void) {
stack_t stack;
//构造(1)native_code_handler_struct 结构体
native_code_handler_struct *const t =
calloc(sizeof(native_code_handler_struct), 1);
if (t == NULL) {
return NULL;
}
ALOGD("installing thread alternative stack 111 \n");
/* Initialize structure *///(2)赋值buffersize,申请buffer
t->stack_buffer_size = SIG_STACK_BUFFER_SIZE;
t->stack_buffer = malloc(t->stack_buffer_size);
if (t->stack_buffer == NULL) {
coffeecatch_native_code_handler_struct_free(t);
return NULL;
}
//(2)初始化一个备用栈
/* Setup alternative stack. */
memset(&stack, 0, sizeof(stack));
stack.ss_sp = t->stack_buffer;
stack.ss_size = t->stack_buffer_size;
stack.ss_flags = 0;
#ifndef NO_USE_SIGALTSTACK
/* Install alternative stack. This is thread-safe */
ALOGD("sigaltstack was called!");
//(3)安装上面定义的备用栈(告诉系统此备用栈的存在),如果之前存在备用栈,则将备用栈保存在t->stack_old
if (sigaltstack(&stack, &t->stack_old) != 0) {
#ifndef USE_SILENT_SIGALTSTACK
coffeecatch_native_code_handler_struct_free(t);
return NULL;
#endif
}
#endif
return t;
}
(1)首先构造了native_code_handler_struct结构体
(2)申请了一个buffer内存 t->stack_buffer
(3)创建了一个栈结构 stack_t 注册到了系统中。当sigaction.flags 指定了SA_ONSTACK 标志时,才会使用这个备用栈
后面通过pthread_setspecific将native_code_handler_struct结构体保存在了线程独有的native_code_thread中。以供后面提取。
pthread_setspecific(native_code_thread, t) != 0)
4、 我们继续看发生cash时 coffeecatch_signal_pass函数中是如何处理信号的
/* Internal signal pass-through. Allows to peek the "real" crash before
* calling the Java handler. Remember than Java needs many of the signals
* (for the JIT, for test-free NullPointerException handling, etc.)
* We record the siginfo_t context in this function each time it is being
* called, to be able to know what error caused an issue.
*/
static void coffeecatch_signal_pass(const int code, siginfo_t *const si,
void *const sc) {
native_code_handler_struct *t;
/* Ensure we do not deadlock. Default of ALRM is to die.
* (signal() and alarm() are signal-safe) */
//(1)首先将发生crash的信号 恢复成默认的行为
signal(code, SIG_DFL);
ALOGD("signal(%d)",code);
//(2)创建一个定时器
coffeecatch_start_alarm();
/* Available context ? */
//(3)提取出存储在线程中的上下文结构体:native_code_handler_struct
t = coffeecatch_get();
ALOGD("coffeecatch_get():%d",t != NULL);
if (t != NULL) {
/* An alarm() call was triggered. */
// ALOGD("coffeecatch_mark_alarm()");
coffeecatch_mark_alarm(t);
/* Take note of the signal. */
coffeecatch_copy_context(t, code, si, sc);
/* Back to the future. */
coffeecatch_try_jump_userland(t, code, si, sc);
}
/* Nope. (abort() is signal-safe) */
ALOGD("calling abort()\n");
signal(SIGABRT, SIG_DFL);
abort();
}
(1)signal(code, SIG_DFL);
将发生crash的信号量 恢复为默认处理行为
C 库函数 void (*signal(int sig, void (*func)(int)))(int) 设置一个函数来处理信号,即带有 sig 参数的信号处理程序
void (*signal(int sig, void (*func)(int)))(int)
sig -- 在信号处理程序中作为变量使用的信号码。
func -- 一个指向函数的指针。它可以是一个由程序定义的函数,也可以是下面预定义函数之一:
SIG_DFL - 默认的信号处理程序。
SIG_IGN - 忽视信号。
(2) coffeecatch_start_alarm();
创建一个定时器 30秒后 终止当前进程
static void coffeecatch_start_alarm(void) {
/* Ensure we do not deadlock. Default of ALRM is to die.
* (signal() and alarm() are signal-safe) */
ALOGD("coffeecatch_start_alarm");
(void) alarm(30);
}
alarm函数脑补:
alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号。可以设置忽略或者不捕获此信号,如果采用默认方式其动作是终止调用该alarm函数的进程。
(3) t = coffeecatch_get();
取出当前线程中的native_code_handler_struct结构体
/* Return the thread-specific native_code_handler_struct structure, or
* @c null if no such structure is available. */
static native_code_handler_struct* coffeecatch_get() {
return (native_code_handler_struct*)
pthread_getspecific(native_code_thread);
}
(4) coffeecatch_mark_alarm(t);
仅仅是做一个标记,表示已经开启了定时器
static void coffeecatch_mark_alarm(native_code_handler_struct *const t) {
t->alarm = 1;
}
(5)coffeecatch_copy_context(t, code, si, sc)
提取crash相关的信息保存在native_code_handler_struct结构体中。
提取的信息包括:
- signal number
- signal code
- 发生crash的 pc
- crash的堆栈信息
(6)coffeecatch_try_jump_userland(t, code, si, sc);
做了两件事情:
-
coffeecatch_revert_alternate_stack();指定不使用备用栈。
-
siglongjmp,跳转回发生crash的pc地址
/* Try to jump to userland. */
static void coffeecatch_try_jump_userland(native_code_handler_struct*
const t,
const int code,
siginfo_t *const si,
void * const sc) {
/* Valid context ? */
if (t != NULL && t->ctx_is_set) {
ALOGD("calling siglongjmp-----1\n");
/* Invalidate the context */
t->ctx_is_set = 0;
//(1)恢复备用栈
/* We need to revert the alternate stack before jumping. */
coffeecatch_revert_alternate_stack();
//(2)跳转回crash发生时的pc地址
siglongjmp(t->ctx, code);
}
}
siglongjmp和sigsetjmp脑补
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
函数说明:sigsetjmp()会保存目前堆栈环境,然后将目前的地址作一个记号,而在程序其他地方调用siglongjmp()时便会直接跳到这个记号位置,然后还原堆栈,继续程序的执行。
参数env为用来保存目前堆栈环境,一般声明为全局变量
参数savesigs若为非0则代表搁置的信号集合也会一块保存
当sigsetjmp()返回0时代表已经做好记号上,若返回非0则代表由siglongjmp()跳转回来。
void siglongjmp(sigjmp_buf env, int val);
理解此处需要结果最初sigsetjmp()的调用
if (coffeecatch_inside() || \
(coffeecatch_setup() == 0 \
&& sigsetjmp(*coffeecatch_get_ctx(), 1) == 0)){
go2Crash4();
}else{
const char*const message = coffeecatch_get_message();
ALOGD("feifei----- enter COFFEE_CATCH :%s",message);
}coffeecatch_cleanup();
在try catch块中
- 调用sigsetjmp,保存了当前的堆栈信息,并做了标记。返回值为0,代表正确做了标记。
- 发生crash,处理完成之后,调用了siglongjmp 跳转回了最初sigsetjmp()的地方。此时返回值非0,因此执行了else分支,在else分支中提取crash的信息:coffeecatch_get_message()
(7)接下来我们再看下coffeecatch_inside()做了哪些事情:
实际上是判断是否在当前线程初始化了coffeecatch环境
即是否在当前线程执行过coffeecatch_handler_setup(1)方法
int coffeecatch_inside() {
native_code_handler_struct *const t = coffeecatch_get();
if (t != NULL && t->reenter > 0) {
t->reenter++;
ALOGD("coffeecatch_inside return 1");
return 1;
}
ALOGD("coffeecatch_inside return 0");
return 0;
}
至此 CoffeeCatch的主要调用流程已经完成