1

Android NativeCrash 捕获与解析

 3 years ago
source link: https://segmentfault.com/a/1190000039273568
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Android 开发中,NE一直是不可忽略却又异常难解的一个问题,原因是这里面涉及到了跨端开发和分析,需要同时熟悉 Java,C&C++,并且需要熟悉 NDK开发,并且解决起来不像 Java异常那么明了,本文为了解决部分疑惑,将从NE的捕获,解析与还原等三个方面进行探索。

一、NE 简介

NE全称NativeCrash,就是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 的体积也会缩小。

如下图所示:

vUj6VvN.png!mobile

如下可以看到strip之前和之后的大小对比。

3IvIF3y.png!mobile

如果对 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。

file libbreakpad-core-s.so
libbreakpad-core-s.so: *******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, stripped
file libbreakpad-core.so
libbreakpad-core.so: ******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, with debug_info, not stripped

如果你是 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_

RfEvQza.png!mobile

另外也可以通过Android SDK提供的工具aarch64-linux-android-strip手动进行strip,aarch64-linux-android-strip这个工具位于/Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains目录下。

这个工具有多种版本,主要针对不同的手机CPU架构,如果不知道手机的CPU架构,可以连接手机使用以下命令查看:

adb shell cat /proc/cpuinfo
Processor   : AArch64 Processor rev 12 (aarch64)

如上图可以看到我的手机CPU用的是aarch64,所以使用aarch64对应的工具aarch64-linux-android-strip,由于NDK提供了很多工具,后续都依照此原则使用即可:

aarch64架构
/Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip
arm架构
/Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-android-strip

使用如下命令可以直接将debug的so进行strip

aarch64-linux-android-strip --strip-all libbreakpad-core.so

使用Cmake进行编译的时候,可以增加如下命令,可以直接编译出strip的so

#set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -s")
#set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -s")

使用mk文件进行编译的时候,可以增加如下命令,也可以直接编译出strip的so

-fvisibility=hidden

二、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下的地址为

/Users/XXXX/Library/Android/sdk/ndk/21.3.6528147/ndk-stack

然后在该目录下执行控制台命令,或者在 Android Studio的terminal中执行也可

adb shell logcat | androidsdk绝对路径/ndk-stack -sym so所在目录

如此控制台在应用发生NE的时候便会输出如下日志,由日志可以看出,崩溃对应的so以及对应的方法名,如果有c的源码,那么就很容易定位问题。

promote:~ njvivo$ adb shell logcat | ndk-stack -sym libbreakpad-core.so
********** Crash dump: **********
Build fingerprint: 'vivo/PD1809/PD1809:8.1.0/OPM1.171019.026/compil04252203:user/release-keys'
#00 0x00000000000161a0 /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/lib/arm64/libbreakpad-core.so (Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo+16)
#01 0x00000000000090cc /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/oat/arm64/base.odex (offset 0x9000)
Crash dump is completed

其实ndk-stack这个工具原理就是内部集成利用了addr2line来实时解析堆栈并且显示在控制台中。

看到这里有的小伙伴就觉得那这个不是很简单,但是实际的崩溃场景一是不容易复现,二是用户的场景有时候很难模拟,那么线上的NE崩溃又该如何监测和定位呢,有两种方式。

2.2 通过DropBox日志解析--适用于系统应用

这个很简单,DropBox会记录JE,NE,ANR的各种日志,只需要将DropBox下面的日志传上来即可进行分析解决,下面贴上一份日志示例。

zQ7BVjI.png!mobile

解析方案1:

借助上述的ndk-stack工具,可以直接将DropBox下面的日志解析成堆栈,从中可以看出,崩溃在breakpad.cpp第111行的Crash()方法中。

ndk-stack -sym /Users/njvivo/Desktop/NE -dump [email protected]
********** Crash dump: **********
Build fingerprint: 'vivo/PD1809/PD1809:8.1.0/OPM1.171019.026/compil04252203:user/release-keys'
#00 0x00000000000161a0 /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/lib/arm64/libbreakpad-core.so (Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo+16)
Crash()
/Users/njvivo/Documents/project/Breakpad/breakpad-build/src/main/cpp/breakpad.cpp:111:8
Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo
/Users/njvivo/Documents/project/Breakpad/breakpad-build/src/main/cpp/breakpad.cpp:122:0
#01 0x00000000000090cc /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/oat/arm64/base.odex (offset 0x9000)
Crash dump is completed

解析方案2:

还是利用Android/SDK/NDK提供的工具linux-android-addr2line,这个工具位于/Users/njvivo/Library/Android/sdk/ndk目录下,有两个版本。

aarch64架构
/Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
arm架构
/Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line

命令使用方法如下,结合未被strip的so以及日志里面出现的堆栈符号00000000000161a0,同样可以解析出崩溃地址和方法。

aarch64-linux-android-addr2line -f -C -e libbreakpad-core.so 00000000000161a0
 
Crash()
/Users/njvivo/Documents/project/Breakpad/breakpad-build/src/main/cpp/breakpad.cpp:111

基于以上,看似也很简单,但是有一个致命的问题就是DropBox只有系统应用能访问,非系统应用根本拿不到日志,那么,非系统应用该怎么办呢?

2.3 通过BreakPad捕获解析--适用于所有应用

非系统应用可以通过google提供的开源工具BreakPad进行监测分析,CrashSDK也是采用的此种方式,可以实时监听到NE的发生,并且记录相关的文件, 从而可以将崩溃和相应的应用崩溃时的启动、场景等结合起来上报。

下面简单介绍一下BreakPad的使用方式。

2.3.1 BreakPad的实现功能

BreakPad主要提供两个个功能,NE的监听和回调,生成minidump文件,也就是dmp结尾的文件,另外提供两个工具,符号表工具和堆栈还原工具。

b2uYjqF.png!mobile

  • 符号表工具: 用于从so中提取出debug信息,获取到堆栈对应的符号表。
  • 堆栈还原工具: 用于将BreakPad生成的dump文件还原成符号,也就是堆栈偏移值。

这两个工具会在编译BreakPad源码的时候产生。

编译完之后会产生minidump_stackwalk工具,有些同学不想编译的话,Android Studio本身也提供了这个工具。

这个minidump_stackwalk程序在Android Studio的目录下面也存在,可以拿出来直接使用,如果不想编译的话,直接到该目录下面取即可,Mac路径为:

/Applications/Android Studio.app/Contents/bin/lldb/bin/minidump_stackwalk

2.3.2 BreakPad的捕获原理

由上述可以得知,BreakPad在应用发生NE崩溃时,可以将NE对应的minidump文件写入到本地,同时会回调给应用层,应用层可以针对本次崩溃做一些处理,达到捕获统计的作用,后续将minidump文件上传之后结合minidump_stackwalk以及addr2line工具可以还原出实际堆栈,示意图如下:

ymQj2q.png!mobile

在应用发生NE时,BreakPad会在手机本地生成一个dump文件,如图所示:

uqArIje.png!mobile

得到了以上文件,我们只能知道应用发生了NE,但是这些文件其实是不可读的,需要解析这些文件。

下面着重讲一下如何分析上面产生的NE:

2.3.3 解析dump文件

1、获取NE崩溃的dump文件,将刚才得到的minidump_stackwalk和dump文件放在同一个目录,也可以不放,填写路径的时候填写绝对路径即可。

然后在该目录下的终端窗口执行以下命令,该命令表示用minidump_stackwalk解析dump文件,解析后的信息输出到当前目录下的crashLog.txt文件。

./minidump_stackwalk xxxxxxxx.dmp >crashLog.txt

2、执行完之后,minidump_stackwalk会将NE的相关信息写到crashLog.txt里面,详细信息如图所示:

6fyYvm.png!mobile

3、根据解析出的NE信息,关注图中红框,可以得知,这个崩溃发生的 libbreakpad-core.so 里面,0x161a0代表崩溃发生在相对根位置偏移161a0的位置

2.3.4 获取崩溃堆栈

1、利用之前提到的addr2line工具,可以根据发生Crash的so文件以及偏移地址(0x161a0)可以得出产生crash的方法、行数和调用堆栈关系。

2、在其根目录对的终端窗口运行以下命令。

arm-linux-androideabi-addr2line -C -f -e ${SOPATH} ${Address}
-C -f           //打印错误行数所在的函数名称
-e                //打印错误地址的对应路径及行数
${SOPATH}         //so库路径
${Address}        //需要转换的堆栈错误信息地址,可以添加多个,但是中间要用空格隔开,例如:0x161a0

3、如下图是真实运行的示例

aarch64-linux-android-addr2line -f -C -e libbreakpad-core.so 0x161a0
Crash()
/Users/njvivo/Documents/project/Breakpad/breakpad-build/src/main/cpp/breakpad.cpp:111

由上图可以知道,该崩溃发生在breakpad.cpp文件的第111行,函数名是Crash(),与真实的文件一致,崩溃代码如下:

void Crash() {
    volatile int *a = (int *) (NULL);
    *a = 1; //此处在代码里是111行
}
 
extern "C"
JNIEXPORT void JNICALL
Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo(JNIEnv *env, jobject instance,
                                                        jstring mLaunchInfoStr_) {
 
 
    DO_TRY
    {
        Crash();
        const char *mLaunchInfoStr = env->GetStringUTFChars(mLaunchInfoStr_, 0);
        launch_info = (char *) mLaunchInfoStr;
//        env->ReleaseStringUTFChars(mLaunchInfoStr_, mLaunchInfoStr);
    }
    DO_CATCH("updateLaunchInfo");
 
}

基于以上,便可以通过应用收集的dump文件解析的NE的详细堆栈信息。

三、so 符号表的提取

3.1 提取 so 的符号表

通过以上内容,我们知道,so中包含了一些debug信息,又叫做符号表,那么我们如何将这些debug信息单独剥离出来呢,ndk也给我们提供了相关的工具。

aarch64架构
/Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-objdump
arm架构
/Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-android-objdump

如下是命令运行的方式,通过此命令,可以将so中的debug信息提取到文件中。

promote:~ njvivo$ aarch64-linux-android-objdump -S libbreakpad-core.so > breakpad.asm

3.2 符号表分析

3.2.1 直接分析

如下图所示就是输出的符号表文件,结合上面的log以及下面的符号表文件,我们同样可以分析出堆栈。

如log中所示,已经表明了崩溃地址是161a0,而161a0对应的代码是*a=1,由上面的分析我们已经知道该崩溃是在breakpad.cpp的111行,也就是*a=1的位置,完全符合预期。

backtrace:
#00 pc 00000000000161a0 /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/lib/arm64/libbreakpad-core.so (Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo+16)
#01 pc 00000000000090cc /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/oat/arm64/base.odex (offset 0x9000)

bUR7rmr.png!mobile

3.2.2 工具解析

google提供了一个Python的工具,将符号表和log结合起来可以直接分析出堆栈,python工具访问 https://code.google.com/archive/p/android-ndk-stacktrace-analyzer/ 可以进行下载。

执行命令,就可以解析出相关堆栈,该工具可以用于服务端批量进行解析,此处不再详细说明。

python parse_stack.py <asm-file> <logcat-file>

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK