目录

JNI Pending Exception

今天遇到了一个 jni pending exception,好在之前也遇到过,所以我当即就知道肯定是我这行 jni 调用之前就已经出现了 java exception

但是为什么会走到我的代码中?这就是个巧合的事情了

什么是 JNI Pending Exception

其实我们每次的 jni 调用都会有一次 check,崩溃调用栈也会发现

我的调用栈如下:

  0x76480  /system/lib64/libc.so (pthread_kill)
  0x249a0  /system/lib64/libc.so (raise)
  0x1ce8c  /system/lib64/libc.so (abort)
  0x47f200  /system/lib64/libart.so (_ZN3art7Runtime5AbortEPKc)
  0xe7acc  /system/lib64/libart.so (_ZN3art10LogMessageD2Ev)
  0x325ad0  /system/lib64/libart.so (_ZN3art9JavaVMExt8JniAbortEPKcS2_)
  0x325b4c  /system/lib64/libart.so (_ZN3art9JavaVMExt9JniAbortVEPKcS2_St9__va_list)
  0x1056c4  /system/lib64/libart.so (_ZN3art11ScopedCheck6AbortFEPKcz)
  0x1050bc  /system/lib64/libart.so (_ZN3art11ScopedCheck11CheckThreadEP7_JNIEnv)
  0x1027a8  /system/lib64/libart.so (_ZN3art11ScopedCheck5CheckERNS_18ScopedObjectAccessEbPKcPNS_12JniValueTypeE)
  0xf7934  /system/lib64/libart.so (_ZN3art8CheckJNI12IsSameObjectEP7_JNIEnvP8_jobjectS4_)
  0x1bc0  /data/app/com.xingin.xhs-2/lib/arm64/libnativebitmap.so (_ZN10BitmapHook24deleteWeakGlobalRefProxyEP7_JNIEnvP8_jobject)
  0x1067d4  /system/lib64/libart.so (_ZN3art8CheckJNI9DeleteRefEPKcP7_JNIEnvP8_jobjectNS_15IndirectRefKindE)
  0x3b1c8  /system/lib64/libmedia_jni.so (_ZN7android4JDrmD2Ev)
  0x3b364  /system/lib64/libmedia_jni.so (_ZTv0_n24_N7android4JDrmD0Ev)
  0xe5d0  /system/lib64/libutils.so (_ZNK7android7RefBase9decStrongEPKv)
  0x3ca7c  /system/lib64/libmedia_jni.so ()
  0x24220d8  /system/framework/arm64/boot-framework.oat (android.media.MediaDrm.native_setup)
  0x24214d8  /system/framework/arm64/boot-framework.oat (android.media.MediaDrm.<init>)
  0xd2e34  /system/lib64/libart.so (art_quick_invoke_stub)
  0xdff50  /system/lib64/libart.so (_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc)
  0x2bb3ec  /system/lib64/libart.so (_ZN3art11interpreter34ArtInterpreterToCompiledCodeBridgeEPNS_6ThreadEPNS_9ArtMethodEPKNS_7DexFile8CodeItemEPNS_11ShadowFrameEPNS_6JValueE)
  0x2b4490  /system/lib64/libart.so (_ZN3art11interpreter6DoCallILb0ELb1EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE)
  0x2d3594  /system/lib64/libart.so (_ZN3art11interpreterL8DoInvokeILNS_10InvokeTypeE1ELb0ELb1EEEbPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE)
  0x2c93d4  /system/lib64/libart.so (_ZN3art11interpreter17ExecuteSwitchImplILb1ELb0EEENS_6JValueEPNS_6ThreadEPKNS_7DexFile8CodeItemERNS_11ShadowFrameES2_b)
  0x28bca8  /system/lib64/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadEPKNS_7DexFile8CodeItemERNS_11ShadowFrameENS_6JValueEb)
  0x291a94  /system/lib64/libart.so (_ZN3art11interpreter33ArtInterpreterToInterpreterBridgeEPNS_6ThreadEPKNS_7DexFile8CodeItemEPNS_11ShadowFrameEPNS_6JValueE)
  0x2b4470  /system/lib64/libart.so (_ZN3art11interpreter6DoCallILb0ELb1EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE)
  0x2d3594  /system/lib64/libart.so (_ZN3art11interpreterL8DoInvokeILNS_10InvokeTypeE1ELb0ELb1EEEbPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE)
  0x2c93d4  /system/lib64/libart.so (_ZN3art11interpreter17ExecuteSwitchImplILb1ELb0EEENS_6JValueEPNS_6ThreadEPKNS_7DexFile8CodeItemERNS_11ShadowFrameES2_b)
  0x28bca8  /system/lib64/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadEPKNS_7DexFile8CodeItemERNS_11ShadowFrameENS_6JValueEb)
  0x291a94  /system/lib64/libart.so (_ZN3art11interpreter33ArtInterpreterToInterpreterBridgeEPNS_6ThreadEPKNS_7DexFile8CodeItemEPNS_11ShadowFrameEPNS_6JValueE)
  0x2b3cbc  /system/lib64/libart.so (_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE)
  0x5b4098  /system/lib64/libart.so (MterpInvokeVirtualQuick)
  0xc9594  /system/lib64/libart.so (ExecuteMterpImpl)

其中你会看到,checkThread 之后就 abort 了

搜索源码可以知道 pending exception 的位置:

// check_jni.cc
 bool CheckThread(JNIEnv* env) REQUIRES_SHARED(Locks::mutator_lock_) {
    Thread* self = Thread::Current();
    CHECK(self != nullptr);

    // Get the current thread's JNIEnv by going through our TLS pointer.
    JNIEnvExt* threadEnv = self->GetJniEnv();

    // Verify that the current thread is (a) attached and (b) associated with
    // this particular instance of JNIEnv.
    // 省略代码

    // Verify that, if this thread previously made a critical "get" call, we
    // do the corresponding "release" call before we try anything else.
    // 省略代码

    // Verify that, if an exception has been raised, the native code doesn't
    // make any JNI calls other than the Exception* methods.
    if ((flags_ & kFlag_ExcepOkay) == 0 && self->IsExceptionPending()) {
      mirror::Throwable* exception = self->GetException();
      AbortF("JNI %s called with pending exception %s",
             function_name_,
             exception->Dump().c_str());
      return false;
    }
    return true;
  }

所以如果发现已经有一个 exception 被抛出了,那么后续所有的 jni 调用都会 crash,都会抛出 jni xxx called with pending exception。

这就是 jni pending exception 的原理了

解决问题

既然我们已经知道了 jni pending exception 表示:之前有 exception 发生,所以本次 jni 调用必 crash。

那么,我们只要找到他为什么会 crash 就好了

我这个例子比较特殊

NI DETECTED ERROR IN APPLICATION: JNI IsSameObject called with pending exception android.media.UnsupportedSchemeException: Failed to instantiate drm object.'

异常信息表示是想要初始化一个 drm object,然后因为已经出现了 exception,我接着调用了 env->IsSameObject() 最终 crash

为什么 drm object 初始化失败了呢?
查了一下是 media drm,这东西似乎依赖于自己本机有这个功能,所以没这个功能肯定就初始化失败了,失败就会抛异常,本来应该就直接 crash,然而我们所有的线程库线程都加了 catch 逻辑,所以最终表现是异常在 java 层被捕获。而一旦走进了我的代码中,就成了一个 native crash 最终崩溃。

为什么会走到我的代码里?
这是因为我刚好 hook 了一个 DeleteWeakGlobalRef 的方法,所有调用这个方法的代码都会走到我的逻辑中,而 MediaDrm 对象很神奇,底层有个 JDrm 对象,析构的时候就会调用到这个方法。本来这个方法不应该有任何额外的逻辑,而我为了一些特殊的功能,在某些条件下,会额外调用两个 jni 方法,最终 crash

如何修改?
DeleteWeakGlobalRef 我还是要 hook,不然无法实现我的功能,那如何绕过这个问题呢?那就如果碰到 DeleteWeakGlobalRef 调用的时候已经出现了 exception,我就直接调用原逻辑就好了,不再调用任何额外的 jni 方法。

所以最终只要在开头加上如下代码即可,这样会抛出对应的 java exception,而 java exception 之前已经被 catch 住了,所以最终无事发生

if(env->ExceptionOccured()) {
  // call origin function
  return;
}

总结

jni pending exception 表示的是之前已经有 exception 发生,需要被处理,在这之后的 jni 调用都将会 crash。

所以真正 crash 的原因可能并不在你的代码之中,关键信息就是看 exception 是什么,然后解决对应的 exception

本例比较特殊,因为本应该是 java crash,但是被 catch 了,然后我 hook 了一些通用方法,导致异常逻辑走进了我的代码中,并且代码中调用了 jni 方法,最终变为 native crash,catch 不住,产生崩溃。

所以我最终选择的是:判断如果异常发生,调用原流程 return,否则走我的特殊逻辑。