音视频开发之旅(59)- 捕获收集、定位分析 Native崩溃

2021-11-28 16:07:25 浏览数 (1)

目录

  1. Native崩溃有哪些类型
  2. 如何捕获收集Native崩溃
  3. 如何分析定位Native崩溃
  4. 资料
  5. 收获

我们知道Java崩溃是在Java代码中出现了未捕获异常,导致程序异常退出,常见的异常有:NPE、OOM、ArrayIndexOutOfBoundsException、IllegalStateException、ConcurrentModificationException等等。 还有一类崩溃,也是我们不得不关注,那就是Native层崩溃,这类崩溃不像Java层崩溃那样比较清晰的看出堆栈信息以及具体的崩溃。每当遇到是都要查找分析,写这篇的目的是帮助自己做下记录,也希望能帮到有类似困扰的你,下面我们开始一起学习实践吧。 本文学习实践的demo以张绍文《Android开发高手课》中的例子进行。

一、 Native崩溃有哪些类型

先来造一个Native崩溃,来看下Native的崩溃信息

图片来自: 刀锋铁骑:常见Android Native崩溃及错误原因 我们可以看到有三个相关信息 Signal xx: 代表错误类型,我们可以先从错误类型上初步判断是哪种类型的崩溃,常见的Native崩溃如下。其中 SIGSEGV时遇到的机率基本上最高的。

接下来是寄存器快照,这个直接看不出来问题,而fault addr是比较关键的一个信息,我们后续再分析定位时会用到它。

再接下来时调用堆栈,这个也非常重要,可以直接帮助我们看出Crash的堆栈信息,但是需要有符号表的so才能转为对应的函数名和行数,否则也是比较难看懂。

二、如何捕获收集Native崩溃

常见的Native崩溃捕获工具:Chromium的BreakPad、腾讯的bugly 我们来通过学习实践Breakpad来进行收集Natvie崩溃。Breakpad是一个跨平台的开源项目,这一小节我们来学习实践下如何编译使用.

2.1 我们先来看下Breakpad的工作原理

图片来自: 学会这个绝招,让 C 崩溃无处可逃!

2.2 编译安装过程如下

  1. 下载[Breakpad]源码(https://chromium.googlesource.com/breakpad/breakpad/ /master)
  2. 下载配置depot_tools
  3. Breakpad依赖LSS,下载它(https://github.com/adelshokhy112/linux-syscall-support)并把 LSS 中的 linux_syscall_support.h 文件放至breakpad/src/third_party/lss/ 目录下;
  4. 编译Breakpad ./configure && make && sudo make install

编译安装成功后可以看到生成的生成的/usr/local/bin/minidump_dump和/usr/local/bin/minidump_stackwalk工具,这些命令工具我们在后面定位分析时会用到

2.3 将Breakpad集成到Android项目中

将 google-breakpad 源代码里面的src文件夹拷贝到项目的src/main/cpp目录下; 配置cmake或者makefile,这里我们使用cmake cmake_minimum_required(VERSION 3.4.1)

代码语言:javascript复制
#设置breakpad根路径
set(BREAKPAD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})

#设置头文件的路径
include_directories(${BREAKPAD_ROOT}/src ${BREAKPAD_ROOT}/src/common/android/include)

#归类要编译的cpp代码的文件 
file(GLOB BREAKPAD_SOURCES_COMMON
        ${BREAKPAD_ROOT}/src/client/linux/crash_generation/crash_generation_client.cc
        ${BREAKPAD_ROOT}/src/client/linux/dump_writer_common/thread_info.cc
        ${BREAKPAD_ROOT}/src/client/linux/dump_writer_common/ucontext_reader.cc
        ${BREAKPAD_ROOT}/src/client/linux/handler/exception_handler.cc
        ${BREAKPAD_ROOT}/src/client/linux/handler/minidump_descriptor.cc
        ${BREAKPAD_ROOT}/src/client/linux/log/log.cc
        ${BREAKPAD_ROOT}/src/client/linux/microdump_writer/microdump_writer.cc
        ${BREAKPAD_ROOT}/src/client/linux/minidump_writer/linux_dumper.cc
        ${BREAKPAD_ROOT}/src/client/linux/minidump_writer/linux_ptrace_dumper.cc
        ${BREAKPAD_ROOT}/src/client/linux/minidump_writer/minidump_writer.cc
        ${BREAKPAD_ROOT}/src/client/minidump_file_writer.cc
        ${BREAKPAD_ROOT}/src/common/convert_UTF.c
        ${BREAKPAD_ROOT}/src/common/md5.cc
        ${BREAKPAD_ROOT}/src/common/string_conversion.cc
        ${BREAKPAD_ROOT}/src/common/linux/elfutils.cc
        ${BREAKPAD_ROOT}/src/common/linux/file_id.cc
        ${BREAKPAD_ROOT}/src/common/linux/guid_creator.cc
        ${BREAKPAD_ROOT}/src/common/linux/linux_libc_support.cc
        ${BREAKPAD_ROOT}/src/common/linux/memory_mapped_file.cc
        ${BREAKPAD_ROOT}/src/common/linux/safe_readlink.cc

        )
#归类要编译的汇编文件
file(GLOB BREAKPAD_ASM_SOURCE ${BREAKPAD_ROOT}/src/common/android/breakpad_getcontext.S
        )

set_source_files_properties(${BREAKPAD_ASM_SOURCE} PROPERTIES LANGUAGE C)

#设置生成静态库所需编译的文件
add_library(breakpad STATIC ${BREAKPAD_SOURCES_COMMON} ${BREAKPAD_ASM_SOURCE})
#链接
target_link_libraries(breakpad log)

2.4 添加breakpad的回调

java层的未捕获的异常可以通过UncaughtExceptionHandler 处理,那么使用Breakpad如何捕获Native层的异常呐?

代码语言:javascript复制
 google_breakpad::MinidumpDescriptor descriptor(path);
    static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback, NULL, true, -1);

具体如下:

代码语言:javascript复制
#include <stdio.h>
#include <jni.h>

#include "client/linux/handler/exception_handler.h"
#include "client/linux/handler/minidump_descriptor.h"


void onNativeCrash(const char* info) {
    JNIEnv *env = 0;
    jclass crashPinClass = env->FindClass(
            "com/test/crash/TestCrash");
    if (crashPinClass == NULL){
        return;
    }
    jmethodID crashReportMethod = env->GetStaticMethodID(crashPinClass,
            "onNativeCrash", "(Ljava/lang/String;)V");
    if (crashReportMethod == NULL) {
      return;
    }
    jstring crashInfo = env->NewStringUTF(info);
    env->CallStaticVoidMethod(crashPinClass, crashReportMethod, crashInfo);
}

//崩溃回调
bool DumpCallback(const google_breakpad::MinidumpDescriptor &descriptor,
                  void *context,
                  bool succeeded) {
    onNativeCrash("");
    return succeeded;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_sample_breakpad_BreakpadInit_initBreakpadNative(JNIEnv *env, jclass type, jstring path_) {
    const char *path = env->GetStringUTFChars(path_, 0);

    google_breakpad::MinidumpDescriptor descriptor(path);
    static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback, NULL, true, -1);

    env->ReleaseStringUTFChars(path_, path);
}

然后加载so设置崩溃后生成的dmp文件的存储路径即可。 收集到了崩溃,我们该如何分析呐?下面小节我们继续学习实践。

三、如何分析定位Native崩溃

在讲解几种常用的分析工具之前,我们先来了解下编译生成带符号表的so和不带符号表的so的区别。

我们可以通过file命令来查看他们之间的区别

代码语言:javascript复制
file cmake/debug/obj/arm64-v8a/libcrash-lib.so
 ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=c776759b526457b5777fc8833f7fc0fcc46055cc, with debug_info, not stripped -->没有剥去debug信息,即带符号表
代码语言:javascript复制
file transforms/stripDebugSymbol/debug/0/lib/arm64-v8a/libcrash-lib.so
ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=c776759b526457b5777fc8833f7fc0fcc46055cc, stripped -->剥去debug信息,即没符号表

如果是我们自己开发编译的so,在发布时要把带符号表的so进行备份或者上传,方便分析定位native崩溃。 需要特别注意的是:不同机器打出来的so的md5是不同的,所以发版后要保存下对应的带符号表的so(obj目录下的不同架构的的so)

下面我们来一起学习下,常用的几种工具

3.1 minidump_stackwalk 导出崩溃堆栈信息

就是上面一小节中我们编译产生的命令工具。用法如下:

代码语言:javascript复制
minidump_stackwalk fd311404-a968-4ce0-17d5fa8a-61a8fdf1.dmp libcrash-lib.so >crash.log

生成的crash.log如下

代码语言:javascript复制
CPU: arm64 8 CPUs

GPU: UNKNOWN

Crash reason:  SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available

Thread 0 (crashed)
 0  libcrash-lib.so   0x5e0 -->出错的地址
     x0 = 0x0000007b14ac4e00    x1 = 0x0000007fedde9894
     x2 = 0x0000000000000000    x3 = 0x0000007b14a56c00
     x4 = 0x0000007feddeaa00    x5 = 0x0000007a7e1a7965
@
"crash.log" 2226L, 114492B

我这我们需要下个一工具继续分析,addr2line可以把地址转为对应的函数名和行数。

3.2 addr2line

基本用法如下

代码语言:javascript复制
Usage: aarch64-linux-android-addr2line [option(s)] [addr(s)]
 Convert addresses into line number/file name pairs.
 If no addresses are specified on the command line, they will be read from stdin
 The options are:

  -e --exe=<executable>  Set the input file name (default is a.out) 指定输入文件
  -f --functions         Show function names    显示函数名称

而addr2line命令所在的路径如下,可以根据崩溃信息中的设备的cpu架构来选择对应的addr2line。

代码语言:javascript复制
arm: $NDK_PATH/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
arm64: $NDK_PATH/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin

示例 我们看到3.1节中我们拿到的dump中的崩溃信息是 arm64 ,崩溃地址是0x5e0,下吗我们使用add2line来进行分析下

代码语言:javascript复制
/Users/yangbin/Library/Android/android-ndk-r16b/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -f -e /Users/yangbin/work/avwork/thirdparty/nativecrash/Chapter01/sample/build/intermediates/cmake/debug/obj/arm64-v8a/libcrash-lib.so 0x5e0
_Z5Crashv
/Users/yangbin/work/avwork/thirdparty/nativecrash/Chapter01/sample/.externalNativeBuild/cmake/debug/arm64-v8a/../../../../src/main/cpp/crash.cpp:10

可以看到输出了对应的错误类和行数,再结合错误原因SIGSEGV即可以快速的分析出具体的原因。

3.3 将上述过程脚本化

新建一个脚本 dumptool.sh,内容如下: 用法如下:dumptool.sh ./test /tmp/fd311404-a968-4ce0-17d5fa8a-61a8fdf1.dmp crash.log 脚本来自:学会这个绝招,让 C 崩溃无处可逃!

代码语言:javascript复制
#!/bin/bash

if [ $# != 3 ] ; then
echo "USAGE: $0 TARGET_NAME DMP_NAME OUTPUT_NAME"
echo " e.g.: $0 test fd311404-a968-4ce0-17d5fa8a-61a8fdf1.dmp crash.log"
exit 1;
fi

#获取输入参数
target_file_name=$1
dmp_file_name=$2
output_file_name=$3


getStackTrace() {
echo "@getStackTrace: start get StackTrace"
sym_file_name=$target_file_name'.sym'

#获取符号文件中的第一行
line1=`head -n1 $sym_file_name`

#从第一行字符串中获取版本号
OIFS=$IFS; IFS=" "; set -- $line1; aa=$1;bb=$2;cc=$3;dd=$4; IFS=$OIFS

version_number=$dd

#创建特定的目录结构,并将符号文件移进去
mkdir -p ./symbols/$target_file_name/$version_number
mv $sym_file_name ./symbols/$target_file_name/$version_number

#将堆栈跟踪信息重定向到文件中
minidump_stackwalk $dmp_file_name ./symbols > $output_file_name
}

main() {
getSymbol
if [ $? == 0 ]
then
getStackTrace
fi
}

3.4 ndk-stack

ndk-stack也是非常有用的工具,它需要结合崩溃时的Tombstone(墓碑文件)进行分析。

ndk-stack用法如下

代码语言:javascript复制
usage: ndk-stack.py [-h] -sym SYMBOL_DIR [-i INPUT]

Symbolizes Android crashes.

optional arguments:
  -h, --help            show this help message and exit
  -sym SYMBOL_DIR, --sym SYMBOL_DIR
                        directory containing unstripped .so files
  -i INPUT, -dump INPUT, --dump INPUT
                        input filename

ndk-stack -sym $PROJECT_PATH/obj/local/armeabi-v7a -dump tombstone.txt

墓碑文件的获取可以通过 adb bugreport来进行获取。 下面我们看下通过命令adb bugreport来拿下墓碑文件,然后结合ndk-stack分析的过程

代码语言:javascript复制
adb bugreport .
unzip bugreport-OnePlus5T-QKQ1.191014.012-2021-11-28-14-49-22.zip
cd FS/data/tombstones
可以看到多个墓碑文件,我们拿最近的一个进行分析

ndk-stack -sym /Users/yangbin/work/avwork/thirdparty/nativecrash/Chapter01/sample/build/intermediates/cmake/debug/obj/arm64-v8a -dump tombstone_09

3.5 IDA Pro

如果没有符号表的so怎么办,可以尝试使用ida这个so逆向分析工具分析定位分析,比如我们用ida打开不带符号表的libcrash-lib.so然后通过错误地址来查询问题

具体驶入如下,我们先用ida打开带符号表的libcrash-lib.so,然后跳转对地址为0x5e0处

我们再用不带符号表的libcrash-lib.so,查看下

可以看到同样也可以定位到对应的类。不过都是一些汇编语言,需要了解下。同样通过另外一个工具objdump也可以同样的找对应的汇编信息,进而继续分析。

这篇基本上就到这里了,文章断更了两个月,这两个月面临岗位变更熟悉,更重要的原因是目标实现了突然放松了,其实这才是起点,通过这两个月工作了解熟悉,音视频涉及的知识和应用真的非常广泛,编解码、渲染、传输、协议、播放器、图形学、AI等等。加油吧少年,下一篇开始我们进入ffmpeg源码解析的系列。尽量做到每周至少一篇,一起学习吧

四、资料

  1. 崩溃优化(上):关于“崩溃”那些事儿
  2. Android 平台 Native 代码的崩溃捕获机制及实现
  3. 学会这个绝招,让 C 崩溃无处可逃!
  4. Android使用Google Breakpad进行崩溃日志管理
  5. Android NDK&JNI开发之Native崩溃日志分析方法
  6. 异常处理 - Native 层的崩溃捕获机制及实现
  7. Android NDK Tombstone/Crash 分析
  8. 安卓Native崩溃定位
  9. Android NDK墓碑/崩溃分析
  10. 如何分析、定位Android Native Crash
  11. 干货|安卓APP崩溃捕获方案——xCrash 对应的开源项目—》[https://github.com/iqiyi/xCrash]
  12. Bugly-Android 平台 Native 代码的崩溃捕获机制及实现
  13. 刀锋铁骑:常见Android Native崩溃及错误原因

五、收获

通过本篇的学习,了解熟悉了如何进行native崩溃的捕获和分析。总结如下:

  1. 学习实践了通过breakpad进行native崩溃的捕获收集
  2. 实践了minidump_stackwalk 把breakpad生成的dump文件转为native崩溃信息文件,然后结合使用add2line和带符号表的对应的so,解析出崩溃的类以及对应的行数
  3. 实践了墓碑文件的获取以及结合ndk_stack进行natvie崩溃堆栈解析
  4. 实践了通过IDA pro分析无符号表的so

感谢你的阅读

下一篇我们再次进入ffmpeg系列,结合源码层面学习解析。

0 人点赞