音视频开发之旅(59)- 捕获收集、定位分析 Native崩溃
目录
Native崩溃有哪些类型
如何捕获收集Native崩溃
如何分析定位Native崩溃
资料
收获
我们知道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 编译安装过程如下
下载[Breakpad]源码(chromium.googlesource.com/breakpad/br…)
下载配置depot_tools
Breakpad依赖LSS,下载它(github.com/adelshokhy1…)并把 LSS 中的 linux_syscall_support.h 文件放至breakpad/src/third_party/lss/ 目录下;
编译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)
#设置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层的异常呐?
google_breakpad::MinidumpDescriptor descriptor(path); static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback, NULL, true, -1);复制代码
具体如下:
#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命令来查看他们之间的区别
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信息,即带符号表 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 导出崩溃堆栈信息
就是上面一小节中我们编译产生的命令工具。用法如下:
minidump_stackwalk fd311404-a968-4ce0-17d5fa8a-61a8fdf1.dmp libcrash-lib.so >crash.log复制代码
生成的crash.log如下
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
基本用法如下
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。
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来进行分析下
/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++ 崩溃无处可逃!
#!/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用法如下
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分析的过程
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源码解析的系列。尽量做到每周至少一篇,一起学习吧
四、资料
崩溃优化(上):关于“崩溃”那些事儿
Android 平台 Native 代码的崩溃捕获机制及实现
学会这个绝招,让 C++ 崩溃无处可逃!
Android使用Google Breakpad进行崩溃日志管理
Android NDK&JNI开发之Native崩溃日志分析方法
异常处理 - Native 层的崩溃捕获机制及实现
Android NDK Tombstone/Crash 分析
安卓Native崩溃定位
Android NDK墓碑/崩溃分析
如何分析、定位Android Native Crash
干货|安卓APP崩溃捕获方案——xCrash
对应的开源项目—》\[[https://github.com/iqiyi/xCrash\]](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fiqiyi%2FxCrash%255D)复制代码
Bugly-Android 平台 Native 代码的崩溃捕获机制及实现
刀锋铁骑:常见Android Native崩溃及错误原因
五、收获
通过本篇的学习,了解熟悉了如何进行native崩溃的捕获和分析。总结如下:
学习实践了通过breakpad进行native崩溃的捕获收集
实践了minidump_stackwalk 把breakpad生成的dump文件转为native崩溃信息文件,然后结合使用add2line和带符号表的对应的so,解析出崩溃的类以及对应的行数
实践了墓碑文件的获取以及结合ndk_stack进行natvie崩溃堆栈解析
实践了通过IDA pro分析无符号表的so
感谢你的阅读
下一篇我们再次进入ffmpeg系列,结合源码层面学习解析。欢迎关注公众号“音视频开发之旅”,一起学习成长。
伪原创工具 SEO网站优化 https://www.237it.com/
作者:音视频开发之旅
链接:https://juejin.cn/post/7035524616382578719