开源库

CoffeeCatch 原理分析

2020-03-06  本文已影响0人  feifei_fly

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 表示开启备用栈,信号处理函数在备用栈上运行

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结构体中。
提取的信息包括:

(6)coffeecatch_try_jump_userland(t, code, si, sc);

做了两件事情:

/* 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块中

(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的主要调用流程已经完成

三、测试代码Demo

https://github.com/feifei-123/TestUnwind

上一篇下一篇

猜你喜欢

热点阅读