JNI Crash:异常定位与捕获处理

96
JackMeGo
2018.07.06 11:50* 字数 2478
crash
关键词:JNI Crash,异常检测,信号量捕获

在Android JNI开发中,经常会遇到JNI崩溃的问题,尤其带代码量大,或者嵌入了第三方代码的情况下,很难进行问题定位和处理。本文将介绍两种常见的JNI崩溃处理方法,包括:

  1. 每个JNI调用后进行异常检测处理(适用于JNI代码量很小的情况)
  2. 捕获系统崩溃的Signal,并进行异常处理(适用于JNI代码量大,难以每句话后面都进行异常检测的情况)

本文github源码地址

下面将分别介绍两种方法:

方法一:ExceptionCheck机制

首先需要理解的是,JNI没有try...catch...finally机制,不能利用这种方法将整段的代码进行异常捕获。

在JNI调用中,如果发生异常,程序并不会停止执行,而是继续执行下一句代码,直到崩溃发生。正确的处理方法是在每一句JNI调用后面都通过ExceptionCheck函数手动检测是否发生了异常,如果检测到异常,进行异常处理。如下:

JNIEXPORT jint JNICALL Java_jack_com_jniexceptionhandler_Calculate_jniDivide
  (JNIEnv * env, jobject jobj, jint m, jint n) {
    char* a = NULL;
    int val1 = a[1] - '0';

    // 每句jni执行之后都加入异常检查
    if (checkExc(env)) {
        LOGE("jni exception happened at p0");
        JNU_ThrowByName(env, "java/lang/Exception", "exception from jni: jni exception happened at p0");
        return -1;
    }

    char* b = NULL;
    int val2 = b[1] - '0';

    // 每句jni执行之后都加入异常检查
    if (checkExc(env)) {
        LOGE("jni exception happened at p1");
        JNU_ThrowByName(env, "java/lang/Exception", "exception from jni: jni exception happened at p1");
        return -1;
    }
    return val1/val2;
}

这里在每次JNI调用之后都要检测是否发生了异常,检测函数checkExec实现如下:

int checkExc(JNIEnv *env) {
    if(env->ExceptionCheck()) {
        env->ExceptionDescribe(); // writes to logcat
        env->ExceptionClear();
        return 1;
    }
    return -1;
}

如果检测到异常,可以在JNI层将异常抛出到Java层进行处理,JNI代码如下:

void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg)
{
     // 查找异常类
     jclass cls = env->FindClass(name);
     /* 如果这个异常类没有找到,VM会抛出一个NowClassDefFoundError异常 */
     if (cls != NULL) {
         env->ThrowNew(cls, msg);  // 抛出指定名字的异常
     }
     /* 释放局部引用 */
     env->DeleteLocalRef(cls);
 }

这样,JNI抛出的异常就可以在Java层通过Try...Catch捕获,并进行相应的出错提示,Java层代码如下:

public static int callJniDivide(int input1, int input2) {
        try {
            return jniDivide(input1, input2);
        } catch (Exception e) {
            Log.e("JniExceptionHandler", e.toString());
            return -1;
        }
    }

这种方法适用于JNI代码完全可控,并且体量比较小的情况,也就是你可以预测到哪些JNI语句可能会导致异常,从而在这些语句后面加入异常检测和处理。对于代码量大,或者JNI里使用了三方代码的情况,这种异常检测的方法很难实施,因为这种情况下你可能没法找出所有可能出异常的点,或者你压根儿不清楚三方库的代码逻辑,也就不能准确找出插入异常检测代码段的地方。这时候可以使用下面我们要介绍的方法二:信号量捕获机制。

方法二:信号量捕获机制

信号量捕获机制是建立在Linux系统底层的信号机制之上的方法,系统层会在发生崩溃的时候发送一些特定信号,通过捕获并处理这些特定信号,我们就能够避免JNI crash的发生,从而相对优雅的结束程序的执行,缺点是我们只知道JNI代码发生了崩溃,没有办法知道具体是哪句代码导致了崩溃。不过当面对庞大复杂的JNI代码时,利用信号量捕获无法预知的崩溃,从而避免Crash的发生,也是非常有意义的。

下面首先介绍一些Linux信号量机制的原理和基本的操作方法。这里有两个知识点,一是如何捕获特定的信号量,二是如何实现代码的控制点跳转。

基础知识一:信号量机制

Signal是传递到进程(Process)的软件中断信号,操作系统通过信号向一个正在执行的程序报告一些预期的状况,比如引用了无效的地址,或者报告一个异步事件的完成。

GNU C库定义了一系列的信号类型,有些信号标志着程序无法继续正常执行,这些信号就会终止程序执行,另外一些信号则可以默认忽略掉。

如果你的程序有可能触发信号,那你可以定义一个handler,当信号发生时,调用这个handker代码进行处理。

一个进程可以向另外一个进程发送信号,这使得父进程可以终止子进程,或者两个相关联的进程可以通过发送信号实现交流和同步。

生成信号的事件(event)可以归纳为三个类型:错误,外部事件,或者显示的请求。

Signal生成(generated)之后变成pending状态,通常pending很短的时间之后就会发送到订阅了这个信号的进程,但是如果这个Signal被阻塞(blocked)了的话,那就可以长时间处于pending状态,直到取消阻塞(unblock)。一旦取消阻塞,信号就会立即被发送出去。

收到信号后通常有三种处理,忽略这个信号、采取默认的动作、或者定义一个handler进行处理。通过signal或者sigaction函数可以定义进行处理的handler,我们称之为handler捕获了这个信号。

标准的信号分为七个类别,包括:
Program Error Signals
Termination Signals
Alarm Signals
Asynchronous I/O Signals
Job Control Signals
Operation Error Signals等

其中我们主要关注的是Program Error Signals。

可以使用signal或sigaction函数指定处理信号的动作,后者较前者更灵活,可以控制的更加细腻。

signal函数使用

sighandler_t signal (int signum, sighandler t action)

上面是signal函数的定义,第一个参数是要捕获的信号,第二个是采取的动作,上面讲到动作分三类:忽略、默认动作或者自定义的handler,分别对应第二个参数为SIG_DFL、SIG_IGN或者自定义的handler函数。函数返回的是之前对这个信号设置的动作,注意,是之前设置的动作,不是本次设置的动作。
自定义hander函数格式如下:

void handler (int signum) { ... }

使用signal函数如下:

      #include <signal.h>
      void termination_handler (int signum)
      {
        struct temp_file *p;
        for (p = temp_file_list; p; p = p->next)
          unlink (p->name);
      }
      int main (void)
      {
        ...
        if (signal (SIGINT, termination_handler) == SIG_IGN)
              signal (SIGINT, SIG_IGN);
        if (signal (SIGHUP, termination_handler) == SIG_IGN)
              signal (SIGHUP, SIG_IGN);
        if (signal (SIGTERM, termination_handler) == SIG_IGN)
              signal (SIGTERM, SIG_IGN);
        ...
}

在上面的代码中,设置新的动作的之后,判断旧的动作是不是忽略(SIG_IGN),如果是的话,恢复成就的动作,也就是先设置了新的动作,如果发现旧的动作是忽略,就又设置回去。

注意,在BSD信号安装(signal)以后需要显示的卸载,而SVID系统(sysv_signal)上则不需要。为了同时兼容不同的情况,sigaction是个更好的选择,应该尽量使用sigaction。

sigaction的使用

sigaction是signal的升级版,比signal函数提供更精细的控制。跟该功能相关的有一个结构体和一个函数,名称都叫sigaction(奇怪!):

struct sigaction {
    sighandler_t sa_handler; // 和signal函数一样,这里可以是默认(SIG_DFL), 忽略(SIG_IGN)或者一个handler函数指针
    sigset_t sa_mask;// handler处理信号过程中,需要被阻塞的信号集合
    int sa_flags; // 提供了多种多样的标志,可以影响信号的表现
};

int sigaction (int signum, const struct sigaction *restrict action, struct sigaction *restrict old-action)

在函数sigaction中,用到了结构体sigaction。
在结构体sigaction中,sa_handler可以是默认(SIG_DFL), 忽略(SIG_IGN)或者一个handler函数指针,这和signal用法是一样的。sa_mask是handler处理信号过程中,需要被阻塞的信号集合,正在被处理的信号类型不需要加入到这个集合中,因为它会自动被阻塞,只有正在被处理的信号之外的类型才需要加入阻塞信号集中。
在函数sigaction中,第一个参数是要处理的信号,第二个参数就是上面提到的结构体sigaction,里面包含了处理该信号需要执行的动作,和期间需要阻塞的信号集。第三个参数返回的是旧的sigaction结构体。第二第三个参数可以分别或者同事设置成NULL,表明同时设置新的动作,查询旧的动作,或者只执行一种。

下面使用sigaction实现前面signal函数的动能:

      #include <signal.h>
      void termination_handler (int signum)
      {
          struct temp_file *p;
          for (p = temp_file_list; p; p = p->next)
              unlink (p->name);
      }

      int main (void)
      {
          ...
          struct sigaction new_action, old_action;
          /* Set up the structure to specify the new action. */ 
          new_action.sa_handler = termination_handler; 
          sigemptyset (&new_action.sa_mask); 
          new_action.sa_flags = 0;
          sigaction (SIGINT, NULL, &old_action);
          if (old_action.sa_handler != SIG_IGN)
              sigaction (SIGINT, &new_action, NULL);
          sigaction (SIGHUP, NULL, &old_action);
          if (old_action.sa_handler != SIG_IGN)
              sigaction (SIGHUP, &new_action, NULL);
          sigaction (SIGTERM, NULL, &old_action);
          if (old_action.sa_handler != SIG_IGN)
              sigaction (SIGTERM, &new_action, NULL);
        ...
}

block signal的两种方式:sigprocmask或者sigaction的sa_mask。两者的区别在于block发生的时机:
sa_mask方式只会在handler执行的时候block信号集中的信号;
sigprocmask方式会block两个sigprocmask之间代码段执行时的信号。

优先使用sigprocmask和sa_mask方法,代码更简洁,可读性强。

两种方式的样例代码如下:
使用sigprocmask来阻塞主程序关键代码执行过程中到达的信号:

      /* This variable is set by the SIGALRM signal handler. */ 
      volatile sig_atomic_t flag = 0;
      int main (void)
      {
          sigset_t block_alarm;
          ...
          /* Initialize the signal mask. */ 
          sigemptyset (&block_alarm); 
          sigaddset(&block_alarm, SIGALRM);
          while (1) {
              /* Check if a signal has arrived; if so, reset the flag. */ 
              sigprocmask (SIG_BLOCK, &block_alarm, NULL);
              if (flag)
              {
                  flag = 0; 
              }
              sigprocmask (SIG_UNBLOCK, &block_alarm, NULL);
              ... 
          }
          actions-if-not-arrived
}

使用sa_mask阻塞handler函数处理过程中到达的信号:

      #include <signal.h>
      #include <stddef.h>
      void catch_stop ();
      void install_handler (void)
      {
            struct sigaction setup_action;
            sigset_t block_mask;
            sigemptyset (&block_mask);
            /* Block other terminal-generated signals while handler runs. */ 
            sigaddset (&block_mask, SIGINT);
            sigaddset (&block_mask, SIGQUIT); 
            setup_action.sa_handler = catch_stop; 
            setup_action.sa_mask = block_mask;
            setup_action.sa_flags = 0;
            sigaction (SIGTSTP, &setup_action, NULL);
      }

基础知识二:Non-Local Exits

Non-Local Exists指的是嵌套很深的jni代码发生异常后没必要一层层的进入父函数进行异常处理,而是可以直接跳转到最外层指定的代码锚点进行异常处理。
有两种应用场景:
场景一:就是上面提到的在嵌套很深的地方发生异常后,简化异常处理,直接跳到最外层进行处理。
场景二:特定信号的handler捕捉到Signal后,跳转到主函数的特定代码段进行出错处理。

      #include <setjmp.h>
      #include <stdlib.h>
      #include <stdio.h>
      sigjmp_buf main_loop; // 代码锚点标志

      int main (void)
      {
        while (1)
              if (sigsetjmp (main_loop))  // 代码锚点
                  puts ("Back at main loop....");
              else
                  do_command ();
      }

      void do_command (void)
      {
            char buffer[128];
            if (fgets (buffer, 128, stdin) == NULL)
                  siglongjmp (main_loop,  -1); // 跳转到锚点执行代码
            else
                  exit (EXIT_SUCCESS);
      }

利用上面的两个知识点通过信号量进行Android jni崩溃捕获和处理

有了上面的基础,我们就可以通过捕捉系统信号量进行JNI崩溃捕获了。完整的代码如下:

#include <signal.h>
#include <setjmp.h>
#include <pthread.h>

/*
jni捕获异常的方法之二:捕捉系统崩溃信号,适用于代码量大的情况。
*/

// 定义代码跳转锚点
sigjmp_buf JUMP_ANCHOR;
volatile sig_atomic_t error_cnt = 0;

void exception_handler(int errorCode){
      error_cnt += 1;
      LOGE("JNI_ERROR, error code %d, cnt %d", errorCode, error_cnt);

      // DO SOME CLEAN STAFF HERE...

      // jump to main function to do exception process
      siglongjmp(JUMP_ANCHOR, 1);
}

jint process(JNIEnv * env, jobject jobj, jint m, jint n) {
    char* a = NULL;
    int val1 = a[1] - '0';

    char* b = NULL;
    int val2 = b[1] - '0';

    LOGE("val 1 %d", val1);
    return val1/val2;
}

JNIEXPORT jint JNICALL Java_trio_com_jniexceptionhandler_Calculate2_jniDivide
  (JNIEnv * env, jobject jobj, jint m, jint n) {
  // 注册需要捕获的异常信号
        /*
         1    HUP Hangup                        33     33 Signal 33
         2    INT Interrupt                     34     34 Signal 34
         3   QUIT Quit                          35     35 Signal 35
         4    ILL Illegal instruction           36     36 Signal 36
         5   TRAP Trap                          37     37 Signal 37
         6   ABRT Aborted                       38     38 Signal 38
         7    BUS Bus error                     39     39 Signal 39
         8    FPE Floating point exception      40     40 Signal 40
         9   KILL Killed                        41     41 Signal 41
        10   USR1 User signal 1                 42     42 Signal 42
        11   SEGV Segmentation fault            43     43 Signal 43
        12   USR2 User signal 2                 44     44 Signal 44
        13   PIPE Broken pipe                   45     45 Signal 45
        14   ALRM Alarm clock                   46     46 Signal 46
        15   TERM Terminated                    47     47 Signal 47
        16 STKFLT Stack fault                   48     48 Signal 48
        17   CHLD Child exited                  49     49 Signal 49
        18   CONT Continue                      50     50 Signal 50
        19   STOP Stopped (signal)              51     51 Signal 51
        20   TSTP Stopped                       52     52 Signal 52
        21   TTIN Stopped (tty input)           53     53 Signal 53
        22   TTOU Stopped (tty output)          54     54 Signal 54
        23    URG Urgent I/O condition          55     55 Signal 55
        24   XCPU CPU time limit exceeded       56     56 Signal 56
        25   XFSZ File size limit exceeded      57     57 Signal 57
        26 VTALRM Virtual timer expired         58     58 Signal 58
        27   PROF Profiling timer expired       59     59 Signal 59
        28  WINCH Window size changed           60     60 Signal 60
        29     IO I/O possible                  61     61 Signal 61
        30    PWR Power failure                 62     62 Signal 62
        31    SYS Bad system call               63     63 Signal 63
        32     32 Signal 32                     64     64 Signal 64
        */

        // 代码跳转锚点
        if (sigsetjmp(JUMP_ANCHOR, 1) != 0) {
            return -1;
        }

        // 注册要捕捉的系统信号量
        struct sigaction sigact;
        struct sigaction old_action;
        sigaction(SIGABRT, NULL, &old_action);
        if (old_action.sa_handler != SIG_IGN) {
            sigset_t block_mask;
            sigemptyset(&block_mask);
            sigaddset(&block_mask, SIGABRT); // handler处理捕捉到的信号量时,需要阻塞的信号
            sigaddset(&block_mask, SIGSEGV); // handler处理捕捉到的信号量时,需要阻塞的信号

            sigemptyset(&sigact.sa_mask);
            sigact.sa_flags = 0;
            sigact.sa_mask = block_mask;
            sigact.sa_handler = exception_handler;
            sigaction(SIGABRT, &sigact, NULL); // 注册要捕捉的信号
            sigaction(SIGSEGV, &sigact, NULL); // 注册要捕捉的信号
        }

        jint value = process(env, jobj, m, n);
        return value;
}

利用上面的两种方法,我们就可以有的放矢的处理JNI异常了,既可以在我们预测会发生异常的地方提前进行异常检测和处理,又可以全局添加崩溃捕获,作为最后的防线,这样就可以告别JNI Crash问题了。

本文github源码地址

参考:
https://www.gnu.org/software/libc/manual/pdf/libc.pdf

日记本
Web note ad 1