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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,999评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,102评论 1 302
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,709评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,439评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,846评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,881评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,062评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,783评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,517评论 1 248
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,762评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,241评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,236评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,145评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,941评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,965评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,802评论 2 275

推荐阅读更多精彩内容