Android NativeCrash 捕获与解析
Android 开发中,NE 一直是不可忽略却又异常难解的一个问题,原因是这里面涉及到了跨端开发和分析,需要同时熟悉 Java,C&C++,并且需要熟悉 NDK 开发,并且解决起来不像 Java 异常那么明了,本文为了解决部分疑惑,将从 NE 的捕获,解析与还原等三个方面进行探索。
一、NE 简介
NE 全称 NativeException,就是 C 或者 C++运行过程中产生的错误,NE 不同于普通的 Java 错误,普通的 logcat 无法直接还原成可阅读的堆栈,一般没有源码也无法调试。
所以日常应用层的工程师,即使我们内部有云诊断的日志,一般也会忽略 NE 的错误,那么遇到这些问题,作为应用层、对 C++不甚了解的工程师能否解决还原堆栈,能否快速定位或者解决 NE 的问题呢?
下面将着重介绍:
1.1 so 组成
我们先了解一下 so 的组成,一个完整的 so 由 C 代码加一些 debug 信息组成,这些 debug 信息会记录 so 中所有方法的对照表,就是方法名和其偏移地址的对应表,也叫做符号表,这种 so 也叫做未 strip 的,通常体积会比较大。
通常 release 的 so 都是需要经过一个 strip 操作的,这样 strip 之后的 so 中的 debug 信息会被剥离,整个 so 的体积也会缩小。
如下图所示:
如下可以看到 strip 之前和之后的大小对比。
如果对 NE 或者 so 不了解的,可以简单将这个 debug 信息理解为 Java 代码混淆中的 mapping 文件,只有拥有这个 mapping 文件才能进行堆栈分析。
如果堆栈信息丢了,基本上堆栈无法还原,问题也无法解决。
所以,这些 debug 信息尤为重要,是我们分析 NE 问题的关键信息,那么我们在编译 so 时候务必保留一份未被 strip 的 so 或者剥离后的符号表信息,以供后面问题分析,并且每次编译的 so 都需要保存,一旦产生代码修改重新编译,那么修改前后的符号表信息会无法对应,也无法进行分析。
1.2 查看 so 状态
事实上,也可以通过命令行来查看 so 的状态,Mac 下使用 file 命令即可,在命令返回值里面可以查看到 so 的一些基本信息。
如下图所示,stripped 代表是没有 debug 信息的 so,with debug_info, not stripped 代表携带 debug 信息的 so。
如果你是 Windows 系统的话,那么我劝你装一个 Linux 子系统,然后在 Linux 执行同样的命令,同样也可以得到该信息。
接下来看下我们如何获取两种状态下的 so。
1.3 获取 strip 和未被 strip 的 so
目前 Android Studio 无论是使用 mk 或者 Cmake 编译的方式都会同时输出 strip 和未 strip 的 so,如下图是 Cmake 编译 so 产生的两个对应的 so。
strip 之前的 so 路径:build/intermediates/transforms/mergeJniLibs
strip 之后的 so 路径:build/intermediates/transforms/stripDebugSymbol
另外也可以通过 Android SDK 提供的工具 aarch64-linux-android-strip 手动进行 strip,aarch64-linux-android-strip 这个工具位于/Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains 目录下。
这个工具有多种版本,主要针对不同的手机 CPU 架构,如果不知道手机的 CPU 架构,可以连接手机使用以下命令查看:
如上图可以看到我的手机 CPU 用的是 aarch64,所以使用 aarch64 对应的工具 aarch64-linux-android-strip,由于 NDK 提供了很多工具,后续都依照此原则使用即可:
使用如下命令可以直接将 debug 的 so 进行 strip
使用 Cmake 进行编译的时候,可以增加如下命令,可以直接编译出 strip 的 so
使用 mk 文件进行编译的时候,可以增加如下命令,也可以直接编译出 strip 的 so
二、NE 捕获与解析
NE 解析顾名思议就是堆栈解析,当然所有的前提就是需要保存一份带符号表、也就是未被 strip 的 so,如果你只有 strip 之后的 so,那就无能为力了,堆栈基本无法还原了。
一般有以下三种方式可以捕获和还原堆栈。
2.1 logcat 捕获
顾名思义,就是通过 logcat 进行捕获,我们通过 Android Studio 打开 logcat,制造一个 NE,只能看到很多类似 #00 pc 00000000000161a0 的符号,并没有一个可以直接阅读的日志,我们想通过 logcat 直接输出一份可以直接阅读的 log。
可以使用 Android/SDK/NDK 下面提供的一个工具 ndk-stack,它可以直接将 NE 输出的 log 解析为可阅读的日志。
ndk-stack 一般是位于 ndk 的工具下面,Mac 下的地址为
然后在该目录下执行控制台命令,或者在 Android Studio 的 terminal 中执行也可
如此控制台在应用发生 NE 的时候便会输出如下日志,由日志可以看出,崩溃对应的 so 以及对应的方法名,如果有 c 的源码,那么就很容易定位问题。
其实 ndk-stack 这个工具原理就是内部集成利用了 addr2line 来实时解析堆栈并且显示在控制台中。
看到这里有的小伙伴就觉得那这个不是很简单,但是实际的崩溃场景一是不容易复现,二是用户的场景有时候很难模拟,那么线上的 NE 崩溃又该如何监测和定位呢,有两种方式。
2.2 通过 DropBox 日志解析--适用于系统应用
这个很简单,DropBox 会记录 JE,NE,ANR 的各种日志,只需要将 DropBox 下面的日志传上来即可进行分析解决,下面贴上一份日志示例。
解析方案 1:
借助上述的 ndk-stack 工具,可以直接将 DropBox 下面的日志解析成堆栈,从中可以看出,崩溃在 breakpad.cpp 第 111 行的 Crash()方法中。
解析方案 2:
还是利用 Android/SDK/NDK 提供的工具 linux-android-addr2line,这个工具位于/Users/njvivo/Library/Android/sdk/ndk 目录下,有两个版本。
命令使用方法如下,结合未被 strip 的 so 以及日志里面出现的堆栈符号 00000000000161a0,同样可以解析出崩溃地址和方法。
基于以上,看似也很简单,但是有一个致命的问题就是 DropBox 只有系统应用能访问,非系统应用根本拿不到日志,那么,非系统应用该怎么办呢?
2.3 通过 BreakPad 捕获解析--适用于所有应用
非系统应用可以通过 google 提供的开源工具 BreakPad 进行监测分析,CrashSDK 也是采用的此种方式,可以实时监听到 NE 的发生,并且记录相关的文件, 从而可以将崩溃和相应的应用崩溃时的启动、场景等结合起来上报。
下面简单介绍一下 BreakPad 的使用方式。
2.3.1 BreakPad 的实现功能
BreakPad 主要提供两个个功能,NE 的监听和回调,生成 minidump 文件,也就是 dmp 结尾的文件,另外提供两个工具,符号表工具和堆栈还原工具。
符号表工具:用于从 so 中提取出 debug 信息,获取到堆栈对应的符号表。
堆栈还原工具:用于将 BreakPad 生成的 dump 文件还原成符号,也就是堆栈偏移值。
这两个工具会在编译 BreakPad 源码的时候产生。
编译完之后会产生 minidump_stackwalk 工具,有些同学不想编译的话,Android Studio 本身也提供了这个工具。
这个 minidump_stackwalk 程序在 Android Studio 的目录下面也存在,可以拿出来直接使用,如果不想编译的话,直接到该目录下面取即可,Mac 路径为:
2.3.2 BreakPad 的捕获原理
由上述可以得知,BreakPad 在应用发生 NE 崩溃时,可以将 NE 对应的 minidump 文件写入到本地,同时会回调给应用层,应用层可以针对本次崩溃做一些处理,达到捕获统计的作用,后续将 minidump 文件上传之后结合 minidump_stackwalk 以及 addr2line 工具可以还原出实际堆栈,示意图如下:
在应用发生 NE 时,BreakPad 会在手机本地生成一个 dump 文件,如图所示:
得到了以上文件,我们只能知道应用发生了 NE,但是这些文件其实是不可读的,需要解析这些文件。
下面着重讲一下如何分析上面产生的 NE:
2.3.3 解析 dump 文件
1、获取 NE 崩溃的 dump 文件,将刚才得到的 minidump_stackwalk 和 dump 文件放在同一个目录,也可以不放,填写路径的时候填写绝对路径即可。
然后在该目录下的终端窗口执行以下命令,该命令表示用 minidump_stackwalk 解析 dump 文件,解析后的信息输出到当前目录下的 crashLog.txt 文件。
2、执行完之后,minidump_stackwalk 会将 NE 的相关信息写到 crashLog.txt 里面,详细信息如图所示:
3、根据解析出的 NE 信息,关注图中红框,可以得知,这个崩溃发生的 libbreakpad-core.so 里面,0x161a0 代表崩溃发生在相对根位置偏移 161a0 的位置
2.3.4 获取崩溃堆栈
1、利用之前提到的 addr2line 工具,可以根据发生 Crash 的 so 文件以及偏移地址(0x161a0)可以得出产生 crash 的方法、行数和调用堆栈关系。
2、在其根目录对的终端窗口运行以下命令。
3、如下图是真实运行的示例
由上图可以知道,该崩溃发生在 breakpad.cpp 文件的第 111 行,函数名是 Crash(),与真实的文件一致,崩溃代码如下:
基于以上,便可以通过应用收集的 dump 文件解析的 NE 的详细堆栈信息。
三、so 符号表的提取
3.1 提取 so 的符号表
通过以上内容,我们知道,so 中包含了一些 debug 信息,又叫做符号表,那么我们如何将这些 debug 信息单独剥离出来呢,ndk 也给我们提供了相关的工具。
如下是命令运行的方式,通过此命令,可以将 so 中的 debug 信息提取到文件中。
3.2 符号表分析
3.2.1 直接分析
如下图所示就是输出的符号表文件,结合上面的 log 以及下面的符号表文件,我们同样可以分析出堆栈。
如 log 中所示,已经表明了崩溃地址是 161a0,而 161a0 对应的代码是*a=1,由上面的分析我们已经知道该崩溃是在 breakpad.cpp 的 111 行,也就是*a=1 的位置,完全符合预期。
3.2.2 工具解析
google 提供了一个 Python 的工具,将符号表和 log 结合起来可以直接分析出堆栈,python 工具访问https://code.google.com/archive/p/android-ndk-stacktrace-analyzer/ 可以进行下载。
执行命令,就可以解析出相关堆栈,该工具可以用于服务端批量进行解析,此处不再详细说明。
3.2.3 偏移位置简析
上面文章提到了一个偏移位置的概念,笔者对此了解也不多,不过大致有一个概念,C 代码有一个根位置的代码的,每行代码相对根代码都有一个偏移位置。
如上图示例 log 中有一行语句
(Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo+16),+16 就是代表相对 nUpdateLaunchInfo 方法的位置往后偏移 16。
由上图可以看到,nUpdateLaunchInfo 方法的位置是 16190,偏移 16,也就是 16190+10(10 进制的 16 转化 16 进制后为 10)=161a0,同日志输出的一样。
四、总结
以上就是本篇文章的所有内容,主要简述了 so 的一些基础知识,以及 Android 中 NE 的崩溃,捕获解析方案,希望通过该文档对涉及到 NE 相关的小伙伴带来帮助,同时后续 CrashSDK 也会支持相关 NE 的解析功能。
作者:vivo-MaLian
版权声明: 本文为 InfoQ 作者【vivo互联网技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/e52f7ad1aade5073139e3981c】。文章转载请联系作者。
评论