目录
- 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]源码(https://chromium.googlesource.com/breakpad/breakpad/ /master)
- 下载配置depot_tools
- Breakpad依赖LSS,下载它(https://github.com/adelshokhy112/linux-syscall-support)并把 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)
代码语言: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源码解析的系列。尽量做到每周至少一篇,一起学习吧
四、资料
- 崩溃优化(上):关于“崩溃”那些事儿
- 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]
- Bugly-Android 平台 Native 代码的崩溃捕获机制及实现
- 刀锋铁骑:常见Android Native崩溃及错误原因
五、收获
通过本篇的学习,了解熟悉了如何进行native崩溃的捕获和分析。总结如下:
- 学习实践了通过breakpad进行native崩溃的捕获收集
- 实践了minidump_stackwalk 把breakpad生成的dump文件转为native崩溃信息文件,然后结合使用add2line和带符号表的对应的so,解析出崩溃的类以及对应的行数
- 实践了墓碑文件的获取以及结合ndk_stack进行natvie崩溃堆栈解析
- 实践了通过IDA pro分析无符号表的so
感谢你的阅读
下一篇我们再次进入ffmpeg系列,结合源码层面学习解析。