coffeecatch 是一款可以用于crash捕捉的C++庫
它只有兩個文件,實現(xiàn)原理比較簡單。
coffeecatch.h 和coffeecatch.c
一、coffeecatch的基本使用
它的用法類似于try catch結(jié)構(gòu),將可能會發(fā)生crash的代碼 放到try{}塊中,發(fā)生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塊實際是執(zhí)行了如下操作:
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()的作用主要是判斷是否已經(jīng)初始化了coffeecatch的環(huán)境,第一次運行返回false,我們暫且不看。
1、coffeecatch_setup
首先看看coffeecatch_setup做了什么。
從注釋中可以看到 主要是初始化了一個crash handler。為context 做了一個標記,表示已經(jīng)調(diào)用過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;
}
}
我們繼續(xù)看 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) 初始化信號處理函數(shù)
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() 注冊信號量和信號處理函數(shù)
/* Initialize globals. */
if (pthread_mutex_lock(&native_code_g.mutex) != 0) {
return -1;
}
ALOGD("coffeecatch_handler_setup_global\n");
//(1) 初始化信號處理函數(shù)
code = coffeecatch_handler_setup_global();
if (pthread_mutex_unlock(&native_code_g.mutex) != 0) {
return -1;
}
(2)創(chuàng)建了native_code_handler_struct結(jié)構(gòu)體,然后將其保存在了線程獨有Key中
coffeecatch_native_code_handler_struct_init 初始化native_code_handler_struct結(jié)構(gòu)體
pthread_setspecific(native_code_thread, t) != 0
2、我們繼續(xù)看信號量是如何被處理的
/* Internal globals initialization. */
static int coffeecatch_handler_setup_global(void) {
if (native_code_g.initialized++ == 0) {//保證是首次調(diào)用
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參數(shù): SA_SIGINFO 指定使用sa_sigaction (而非sa_handler)作為信號處理函數(shù),SA_ONSTACK 表示開啟備用棧,信號處理函數(shù)在備用棧上運行
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 用于保存 該信號之前安裝的信號處理函數(shù).
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函數(shù)來處理,其他使用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)調(diào)用sigaction函數(shù) 為信號量安裝處理函數(shù)
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;
}
函數(shù)運行在用戶態(tài),當(dāng)遇到系統(tǒng)調(diào)用、中斷或是異常(包括crash)的情況時,內(nèi)核會接收到對應(yīng)的信號,然將其放到對應(yīng)進程的信號隊列中,由對應(yīng)的進程的信號處理函數(shù)來處理該信號。native crash的捕捉也就是在信號處理函數(shù)完成的。
信號機制和Android natvie crash捕捉
腦補sigaction()函數(shù)和sigaction結(jié)構(gòu)體
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); //默認信號處理函數(shù)
void (*sa_sigaction)(int, siginfo_t *, void *); //可以發(fā)送附加信息的信號處理函數(shù),sa_flag設(shè)置了SA_SIGINFO使用其處理
sigset_t sa_mask;//在此信號集中的信號在信號處理函數(shù)運行中會被屏蔽,函數(shù)處理完后才處理該信號
int sa_flags;//可設(shè)參數(shù)很多
void (*sa_restorer)(void);//在man手冊里才發(fā)現(xiàn)有這玩意,還不知道啥用
};
coffeecatch中 共處理的7種信號量:
SIGABRT, SIGILL, SIGTRAP, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT
聲明了兩個sigaction結(jié)構(gòu)體,sa_abort和sa_pass
sigabrt使用coffeecatch_signal_abort函數(shù)來處理,其他信號量使用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; //指定信號處理函數(shù)
sa_abort.sa_flags = SA_SIGINFO | SA_ONSTACK;
//(2)注意此處的flags參數(shù): SA_SIGINFO 指定使用sa_sigaction (而非sa_handler)作為信號處理函數(shù),SA_ONSTACK 表示開啟備用棧,信號處理函數(shù)在備用棧上運行
- sa_abort.sa_sigaction 捕捉到信號量之后的信號處理函數(shù)
- sa_abort.sa_flags SA_SIGINFO 指定使用sa_sigaction (而非sa_handler)作為信號處理函數(shù)
- sa_abort.sa_flags ,SA_ONSTACK 表示開啟備用棧,信號處理函數(shù)在備用棧上運行,而不是運行在系統(tǒng)原有的棧結(jié)構(gòu)上(因為發(fā)生crash時也許系統(tǒng)的棧已經(jīng)溢出,如果繼續(xù)再系統(tǒng)棧上運行可能會引起二次崩潰。
pthread_key_create 又是做了什么呢?
線程私有變量腦補
C 語言中有一種線程獨有數(shù)據(jù)的方式,即只有當(dāng)前線程中可以訪問當(dāng)前線程聲明的變量,其他線程訪問該變量得到的是一個新的值。就相當(dāng)于JAVA 中的ThreadLocal線程獨有變量。
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*))
第一個參數(shù)為指向一個鍵值的指針,第二個參數(shù)指明了一個destructor函數(shù),如果這個參數(shù)不為空,那么當(dāng)每個線程結(jié)束時,系統(tǒng)將調(diào)用這個函數(shù)來釋放綁定在這個鍵上的內(nèi)存塊。
int pthread_setspecific(pthread_key_t key,const void *pointer));
void *pthread_getspecific(pthread_key_t key);
set是把一個變量的地址告訴key,一般放在變量定義之后,get會把這個地址讀出來,然后你自己轉(zhuǎn)義成相應(yīng)的類型再去操作,注意變量的有效期。
一般的處理流程如下:
1、創(chuàng)建一個鍵
2、為一個鍵設(shè)置線程私有數(shù)據(jù)
3、從一個鍵讀取線程私有數(shù)據(jù)void *pthread_getspecific(pthread_key_t key);
4、線程退出(退出時,會調(diào)用destructor釋放分配的緩存,參數(shù)是key所關(guān)聯(lián)的數(shù)據(jù))
5、刪除一個鍵
由此可知 pthread_key_create 創(chuàng)建了一個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;
//構(gòu)造(1)native_code_handler_struct 結(jié)構(gòu)體
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ǒng)此備用棧的存在),如果之前存在備用棧,則將備用棧保存在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)首先構(gòu)造了native_code_handler_struct結(jié)構(gòu)體
(2)申請了一個buffer內(nèi)存 t->stack_buffer
(3)創(chuàng)建了一個棧結(jié)構(gòu) stack_t 注冊到了系統(tǒng)中。當(dāng)sigaction.flags 指定了SA_ONSTACK 標志時,才會使用這個備用棧
后面通過pthread_setspecific將native_code_handler_struct結(jié)構(gòu)體保存在了線程獨有的native_code_thread中。以供后面提取。
pthread_setspecific(native_code_thread, t) != 0)
4、 我們繼續(xù)看發(fā)生cash時 coffeecatch_signal_pass函數(shù)中是如何處理信號的
/* 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)首先將發(fā)生crash的信號 恢復(fù)成默認的行為
signal(code, SIG_DFL);
ALOGD("signal(%d)",code);
//(2)創(chuàng)建一個定時器
coffeecatch_start_alarm();
/* Available context ? */
//(3)提取出存儲在線程中的上下文結(jié)構(gòu)體: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);
將發(fā)生crash的信號量 恢復(fù)為默認處理行為
C 庫函數(shù) void (*signal(int sig, void (*func)(int)))(int) 設(shè)置一個函數(shù)來處理信號,即帶有 sig 參數(shù)的信號處理程序
void (*signal(int sig, void (*func)(int)))(int)
sig -- 在信號處理程序中作為變量使用的信號碼。
func -- 一個指向函數(shù)的指針。它可以是一個由程序定義的函數(shù),也可以是下面預(yù)定義函數(shù)之一:
SIG_DFL - 默認的信號處理程序。
SIG_IGN - 忽視信號。
(2) coffeecatch_start_alarm();
創(chuàng)建一個定時器 30秒后 終止當(dāng)前進程
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函數(shù)腦補:
alarm也稱為鬧鐘函數(shù),它可以在進程中設(shè)置一個定時器,當(dāng)定時器指定的時間到時,它向進程發(fā)送SIGALRM信號??梢栽O(shè)置忽略或者不捕獲此信號,如果采用默認方式其動作是終止調(diào)用該alarm函數(shù)的進程。
(3) t = coffeecatch_get();
取出當(dāng)前線程中的native_code_handler_struct結(jié)構(gòu)體
/* 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);
僅僅是做一個標記,表示已經(jīng)開啟了定時器
static void coffeecatch_mark_alarm(native_code_handler_struct *const t) {
t->alarm = 1;
}
(5)coffeecatch_copy_context(t, code, si, sc)
提取crash相關(guān)的信息保存在native_code_handler_struct結(jié)構(gòu)體中。
提取的信息包括:
- signal number
- signal code
- 發(fā)生crash的 pc
- crash的堆棧信息
(6)coffeecatch_try_jump_userland(t, code, si, sc);
做了兩件事情:
coffeecatch_revert_alternate_stack();指定不使用備用棧。
siglongjmp,跳轉(zhuǎn)回發(fā)生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)恢復(fù)備用棧
/* We need to revert the alternate stack before jumping. */
coffeecatch_revert_alternate_stack();
//(2)跳轉(zhuǎn)回crash發(fā)生時的pc地址
siglongjmp(t->ctx, code);
}
}
siglongjmp和sigsetjmp腦補
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
函數(shù)說明:sigsetjmp()會保存目前堆棧環(huán)境,然后將目前的地址作一個記號,而在程序其他地方調(diào)用siglongjmp()時便會直接跳到這個記號位置,然后還原堆棧,繼續(xù)程序的執(zhí)行。
參數(shù)env為用來保存目前堆棧環(huán)境,一般聲明為全局變量
參數(shù)savesigs若為非0則代表擱置的信號集合也會一塊保存
當(dāng)sigsetjmp()返回0時代表已經(jīng)做好記號上,若返回非0則代表由siglongjmp()跳轉(zhuǎn)回來。
void siglongjmp(sigjmp_buf env, int val);
理解此處需要結(jié)果最初sigsetjmp()的調(diào)用
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塊中
- 調(diào)用sigsetjmp,保存了當(dāng)前的堆棧信息,并做了標記。返回值為0,代表正確做了標記。
- 發(fā)生crash,處理完成之后,調(diào)用了siglongjmp 跳轉(zhuǎn)回了最初sigsetjmp()的地方。此時返回值非0,因此執(zhí)行了else分支,在else分支中提取crash的信息:coffeecatch_get_message()
(7)接下來我們再看下coffeecatch_inside()做了哪些事情:
實際上是判斷是否在當(dāng)前線程初始化了coffeecatch環(huán)境
即是否在當(dāng)前線程執(zhí)行過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的主要調(diào)用流程已經(jīng)完成