聊一聊应用层开发者怎么应对Native Crash

4,398

番外

crash一直是app稳定性最重要的标准之一,通常根据特性,分为java层crash与native层crash,对于java层crash,我们作为应用开发者,其实很容易就能在应用层上进行解决,与之相对的native层crash,却没有那么简单明了,很容易打得我们应用层开发者一个“措手不及”。此时,如果crash的so由第三方提供的话,我们也只能等待第三方进行后续修复。

当然,在笔者的个人经验中,发生的native crash大部分也是由第三方进行提供,我们也没有“直接”手段对这些bug进行处理,与此同时,属于这部分的bug中,大部分是在异步线程调用时发生的,其实很容易理解,这种异步线程调用引起的crash,往往没那么容易在测试环境复现。比如通常在native 子线程中发生crash,比如通常在backtrace文件有类似log。

 #01 pc 00000000000007f0  crash 栈顶的消息xxxx
 #02 pc 00000000000f43f0  /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+64) (BuildId: cea9159973293d0520e9105a73ca766b)
 #03 pc 000000000008ef34  /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: cea9159973293d0520e9105a73ca766b)

我们都知道,这个通常是pthread_create调用,在调用后的子线程运行时“某处”发生了crash。当然,问题可不是出在pthread_create本身调用,而是内部的异步逻辑的问题,那么针对这种特定场景,我们有没有处理办法呢?

兜底方案

安全气囊

安全气囊,比如我们之前提到的,监听信号之后进行一次重启,这样能保证一次用户流程的完整。比如我们之前提到的方案Signal。但是由于安全气囊方案本身的局限性,但是在后台还好,如果在前台,那就是真正有用户感知了,这样会损失用户的体验,但是这个方案也有好处,就是成本足够简单并且通用各种native crash场景。

针对这种case的特定方案

我们的场景足够明确,就是只针对这种异步子线程的场景进行兜底,在native层也能做到类似java层的try catch效果,那么有什么方案呢?有!它来了,这就是今天要讲的代码,mooner,下面要讲解的方案实现代码已经放在github上了。

mooner的方案

我们都知道,java层有try catch帮助我们进行异常的捕获,在异常处理这块,其实算不上一个“真异常”,这是因为java层的异常其实相对于虚拟机来说,都是已知可控的,因此我们有捕获的逻辑,就算引起了我们app运行逻辑的异常,但是也不影响虚拟机本身。而在native层,异常通常以signal 信号的方式进行传递,native层的异常,可是会相当于影响整个虚拟机,比如读写受阻,内存映射错误等等,这些都导致了虚拟机无法再正常进行处理下去了,因此对于native crash来说,反而不那么可控。

但是对于这些native层的crash,我们真的没有一点方法吗?其实也不是,我们在Android性能优化 - SIGSEGV的段错误保护实现(基于sigsetjmp)这篇文章中,有提到,我们也可以在一些情况下,可以采用sigsetjmp与siglongjmp这种非常强力的native层可用的操作,帮助我们回溯到栈某一个稳定的时期,从而避免了本次异常操作。但是这种方式有一个局限,就是我们必须要知道调用方的栈情况,才能采取sigsetjmp与siglongjmp这种方式。我们举个例子

image.png 我们在fun2调用longjmp回到fun1,这是可行的,这是因为当前fun1(setjmp调用方)的调用栈还存在内存上,因此我们回溯可以到达。

但是我们换成一个fun3,即已经脱离了fun1本身调用栈可达的另一个栈帧上发起一次longjmp的话,此时fun1的栈帧很有可能被销毁了,因此就会错误出现SIGSEGV或者SIGABORT

image.png

但是在我们这个场景下,我们只要确保调用栈的关系,那么是可以完全安全的使用sigsetjmp与siglongjmp进行回溯处理。

mooner针对这种异步线程发生的crash,采取的方案是通过hook pthread_create,在我们的的hook函数上进行了sigsetjmp的注入,再调用一次原函数的pthread_create,从而确保了在之后发生信号监听的时候,确保了调用栈的可达。可能大家可能会问,为什么只hook pthread_create,其他的创建线程,比如clone 调用也可以创建线程呀!现在因为mooner还在建设中,同时使用pthread_create创建线程的场景是普遍多的,因此只hook pthread_create也够用了。

我们再给出一些整体的流程:

image.png

pthread_create 的hook

我们明确了方案,那么怎么实现对pthread_create的hook呢?其实很简单,用plthook即可,这个也是最稳定的方案,我们之前也聊过plt hook Android性能优化 - plt hook 与native线程监控

mooner采用的plthook方案是比较成熟与新的bhook,也是xhook作者开发的

最终api展示

最终,通过笔者花费了半天的周末时间,按照上面的方案,已经把相关代码上传到github了,就是我们今天讲的mooner

调用者只需要把lib名称,与需要兜底的信号传入即可,发生兜底时同时也会回调到block中

Mooner.initMooner("libmooner.so",11){
    Log.e("mooner","catch exception")
}

局限

我们再次明确一下,mooner这个方案只针对pthread_create创建的子线程中发生了信号才进行兜底操作,如果是由其他处发生了信号从而导致崩溃,则没有拦截。同时目前还在建设中,目前只支持兜底一个so库的一个信号,当然后面会往一次调用即可兜底多个so库多个信号的方向发展!

当然,对于信号处理器无法处理的信号,比如SIGKILL,mooner也是无法做到拦截的,因为里面就是基于sigaction处理

总结

本篇贴出的代码有点少!没事,已经上传到github上啦,mooner如果对你有帮助,请别忘记给我一个star!同时欢迎后续的pr!