背景: 当线上数据配置异常时,客户端代码未做好兜底走到异常分支就可能触发crash,如果在启动阶段就可能导致大面积的crash
目的: 降低线上crash发生到修复期间的影响,熔断导致crash的业务逻辑,规避应用程序启动闪退导致的应用无法使用问题。
方案实现
java crash系统处理流程
从art角度分析java crash
下面分别打印了主线程和子线程触发crash时调用Thread.dispatchUncaughtException的backtrace (红色的为主线程),通过代码去看一下如何触发堆栈。
异常抛出流程
- 打包阶段 java->class->dex dex中对应的指令是throw
- dex2oat 阶段。 throw->pDeliverException
- 运行时执行阶段pDeliverException-> art_quick_deliver_exception-> artDeliverExceptionFromCode-> Thread::SetException
异常处理流程
->QuickExceptionHandler::FindCatch(从堆栈中找try catch块)
-> CatchBlockStackVisitor::VisitFrame(遍历堆栈,从底层method开始找)
->HandleTryItems(ArtMethod* method)
->ArtMethod::FindCatchBlock (在方法中找到匹配到catch块的dex_pc)
->exception_handler.DoLongJump(清理exception 后跳转到catch块代码执行)
如果找不到catch块处理,就会去通过DetachCurrentThread()和Destroy()去完成,也就是一开始的backtrace ,最终通过调用Thread的dispatchUncaughtException方法执行.
crash降级处理技术原理
通过上面的图,我们能发现最容易打破crash处理流程的点就是在Thread.dispatchUncaughtException,这里我们可以设置自己的一个UncaughtExceptionHandler 来接管流程,各种Crash sdk也是通过这个地方去做crash 捕获。 难道我们只要直接返回就好吗?这边先给出流程图再讨论里面遇到的问题。
- 针对没有looper的子线程,我们确实只要直接return就行,影响的只是子线程的逻辑。
- 而主线程处理完uncaughtException,就算我们不去kill进程,应用程序也会卡死最终ANR。因为主线程执行任务是handler机制,当异常抛出来时looper的任务也不再执行了,此时也无法消费system 进程发过来的消息。 针对上述问题我们可以尝试在uncaughtException下调用Looper.loop重新启用。 但是如果在loop中又抛出了异常。那之前阻塞的逻辑就继续往下走了导致卡死。这时就需要再去try catch Looper.loop这个循环 然后在外面加上while(true)。主线程影响的是一个handler task执行的逻辑。所以类似存在looper逻辑的线程,我们就要想办法把looper重新启动
- 上述方案在兜底一些主线程主流程crash时会导致卡死或白屏,但是启动crash又不可避免会出现在这些常见堆栈中。举个例子我们在app的attach或者oncreate方法中调用了下面方法,这种用上面的方案就不可行了。
如果这时候能加个try catch就好了 比如这样
有没有办法动态去添加呢?
在逆向分析中经常会用到的一个框架叫xposed,也衍生出了LSPosed(github.com/LSPosed/LSP…, 部分原理就是动态生成dex代码,然后在原方法artmethod entry_point_from_quick_compiled_code_处添加hook代码跳转到新生成的代码。
那么我们也可以利用该原理去做代码的动态try catch功能。
连续启动crash的自动尝试保护
一个配置异常导致线上app启动连续crash无法启动这种场景也是有的。 为了避免这种场景,安全气垫也做了连续crash的自动降级保护功能
-
发生启动crash时记录标记,满足连续启动crash时,我们尝试从上面各种case 顺序开启保护。
- 满足子线程无looper条件 直接return
- 主线程非主流程crash, 尝试通过重启loop的方案
- 在启动主线程主流程主中发生的crash,需要去分析堆栈尝试动态try catch
-
触发阶段。启动crash处理时 满足兜底条件 且crash堆栈发生在主线程主流程中。
-
保存错误方法签名。 通过crash堆栈 找到最底部的报错堆栈,然后找到crash方法签名,通过堆栈可能会找到多个重名方法,这边全部保存等到下次启动生效。
-
方法hook阶段。 对上次保存下的方法进行hook ,被hook的方法默认加上了try catch逻辑。 在异常中我们根据returnType 返回一些默认值 简单尝试自修复。 比如 Object void 返回 null, boolean 返回false, int ,short,long ,byte 返回0, 如果还发生crash则会对上一个方法进行try catch
-
稳定性方面,arthook 需要兼容不同的系统版本,存在一些兼容性问题,但本身只做为启动crash兜底的话没什么影响,因为本身就已经要crash了,作为一个抢救措施。
基于堆栈下发crash保护
线上配有全局兜底开关,这也是在极端情况下的抢救措施,正常情况下我们还是需要正常的去收集crash堆栈来暴露线上问题。所以通常情况下我们去兜底保护已知问题是用下发堆栈方法的方式去进行匹配。 比如匹配版本,crash类型,crash线程,厂商,系统版本,方法签名等维度。下方不同策略的保护方式。
举个例子。
issuetracker.google.com/issues/2108… 最近发现android 12上的一个bug 下发了堆栈保护,保护后使用分析工具查看用户后续行为都是正常使用的
业务价值
- 拥有线上问题紧急保护能力,下发兜底crash降级配置,完成线上crash问题保护
- 线上连续启动故障中,成功拦截crash异常, 自动触发兜底
- 通过下发堆栈修复framework crash问题,有效避免App线上崩溃情况