Google Breakpad分析Android Native Crash
友情提示,本文中的代码和命令均在 Mac 中执行
崩溃
崩溃是Android开发经常会碰到的问题,我们都知道,Android崩溃分为Java崩溃和Native崩溃。简单来说Java崩溃就是在Java代码中出现了未捕获异常,导致程序异常退出。那Native崩溃又是如何产生的?一般是因为Native代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动abort,这些都会产生相应的signal信号,导致程序异常退出。
相比于Java崩溃,Native崩溃更难捕获和定位。事实上针对我们目前的项目,几乎大部分的崩溃都是Native 崩溃。分析项目的崩溃日志我们可以发现,有些崩溃是可以从backtrace中获得一些有用的信息。但是有些崩溃甚至连backtrace都不会出现,开发人员只能单步调试,无法快速定位。所以有没有一个系统的框架来收集和处理这些Native崩溃呢?当然有,Google开源的BreakPad就是其中之一,而这个工具也是Google开源的一个跨平台的崩溃转储和分析框架的工具集合,官方推荐,咱当然得试试了。
当然了除了breakpad之外也有一些类似的产品或者工具,都可以用,比如Bugly、友盟、还有我们国内访问不了并使用不了的firebase。
为什么使用Google Breakpad?
在项目开发中,我们经常需要引入一些第三方的.so文件或者是写一些Native代码,但是当Native代码出现crash后,对crash的追踪和定位一直是一个比较艰难的事情。
Google Breakpad是一套完整的工具集,从Crash的捕获到Crash的dump,都提供了相对应的工具。它记录了崩溃时的.dump文件,无论我们是在本地或者发送到服务器端,都可以用相对应的工具来解析.dump文件帮助我们查找C和C++堆栈踪迹。
信号机制
在介绍BreakPad之前,我们首先要对信号机制有一个大概的了解。这里只是希望能对常见的信号类型有一个认识,因为一旦发生崩溃,信号类型能让我们对崩溃有一个初步的判断。
信号量 | value | 描述 | 例子 |
---|---|---|---|
SIGABRT | 6 | 程序发生错误或者调用了abort | 很多C的库函数,如果发现异常会调用abort,如strlen |
SIGBUS | 10,7,10 | 不存在的物理地址硬件错误 | 更多的是因为硬件或者系统引起的 |
SIGFPE | 8 | 浮点数运算错误 | 如除0,余0,整型溢出 |
SIGILL | 4 | 非法指令 | 损坏的可执行文件或者代码区损坏 |
SIGSEGV | 11 | 段地址错误 | 空指针,访问不存在的地址空间,访问内核区,写只读空间,栈溢出,数组越界,野指针 |
SIGSTKFLT | 16 | ||
SIGPIPE | 13 | 管道错误,往没有reader的管道中写 | Linux中的socket,如果断掉了继续写,signal(SIGPIPE,SIG_IGN) |
Google BreakPad简介
Google breakpad是一个跨平台的崩溃转储和分析框架和工具集合。
Breakpad由三个主要组件:
- client,以library的形式内置在你的应用中,当崩溃发生时写 minidump文件
- symbol dumper, 读取由编译器生成的调试信息(debugging information),并生成 symbol file
- processor, 读取 minidump文件 和 symbol file ,生成可读的c/c++ Stack trace.
简单来说就是一个生成 minidump,一个生成symbol file,然后将其合并处理成可读的Stack trace。
MiniDump文件格式
minidump文件格式是由微软开发的用于崩溃上传的一种文件格式,它包括:
- 当dump生成时进程中一系列executable和shared libraries, 包括这些文件的文件名和版本号。
- 进程中的线程列表,对于每个线程,minidump包含它在寄存器中的状态,线程的stack memory内容。这些数据都是未解析的字节流,Breakpad client通常没有调试信息(debugging information)能生成函数名,行号,甚至无法确定stack frame的边界。
- 其他收集关于系统的信息,如:处理器,操作系统高版本,dump的原因等等。
breakpad在所有平台上(windows/linux等)都统一使用minidump文件格式,而不使用core files,原因是因为:
- core files可能很大,而minidump比较小。
- core files文档不全
- 很难说服windows机器去生成core files,但可以说服其他机器来生成minidump文件。
- breakpad只支持一种统一的格式会比较简单,而不是同时支持多种格式。
什么是core files?core files是unit系统上程序崩溃时生成的文件。
Symbols文件格式
symbols文件是基于纯文本的,每一行一条记录,每条记录中的字段以一个空格作为分隔符,每条记录的第一个字段表示这一行是什么类型的记录。
记录类型:
- 模块记录:MODULE operatingsystem architecture id name
- 文件记录:FILE number name
- 函数记录:FUNC address size parameter_size name
- 行号记录:address size line filenum
- PUBLIC记录:PUBLIC address parameter_size name
- STACK WIN
- STACK CFI
参见:https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/symbol_files.md
不同平台的实现原理
默认情况下,当崩溃时breakpad会生成一个minidump文件,在不同平台上的实现机制不一样:
- 在windows平台上,使用微软提供的 SetUnhandledExceptionFilter() 方法来实现。
- 在OS X平台上,通过创建一个线程来监听 Mach Exception port 来实现。
- 在Linux平台上,通过设置一个信号处理器来监听 SIGILL SIGSEGV 等异常信号。
当minidump被生成后,在不同平台上也使用不同的机制来上传crash dump文件。
参见:Windows SetUnhandledExceptionFilter
参见:Mac OS X Exception handling
参见:Catching Exceptions and Printing Stack Traces for C on Windows, Linux, & Mac
异常处理机制
提供两种不同的异常处理机制:
- 同进程(in-process)
- 跨进程(out-precess)
因为在崩溃的进程写minidump文件是不安全的,所以三个平台(windows、linux、mac os)都提供跨进程的异常处理机制。
构建Breakpad库
git clone git@github.com:google/breakpad.git
cd breakpad
./configure && make
编译结束后,src目录下就是所有的产出内容,主要为
- client,以library的形式内置在你的应用中,当崩溃发生时写 minidump文件
- symbol dumper, 读取由编译器生成的调试信息(debugging information),并生成 symbol file
- processor, 读取 minidump文件 和 symbol file ,生成可读的c/c++ Stack trace.
在应用中使用breakpad
- 首先,创建一个Empty Android project
- 再创建一个module
lib_mb_break_pad
- 在
lib_mb_break_pad
中创建src/main/include
目录 - 将 上一步中breakpad的编译产物(src目录下的所有文件)复制到
src/main/include
目录中 - 创建
com.mb.lib.nativecrash.breakpad.BreakPadInit.java
文件
public class BreakPadInit {
static {
System.loadLibrary("breakpad-core");
}
public static void initBreakpad(String path){
initBreakpadNative(path);
}
private static native void initBreakpadNative(String path);
}
- 创建
src/main/cpp/breakpad.cpp
#include <stdio.h>
#include <jni.h>
#include <android/log.h>
#include "../include/client/linux/handler/exception_handler.h"
#include "../include/client/linux/handler/minidump_descriptor.h"
#define LOG_TAG "mb_breakpad_monitor"
#define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
bool DumpCallback(const google_breakpad::MinidumpDescriptor &descriptor,
void *context,
bool succeeded) {
ALOGD("===============mb_breakpad_get_native_crash================");
ALOGD("Dump path: %s\n", descriptor.path());
return succeeded;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_mb_lib_nativecrash_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);
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
- 编写 CmakeLists文件,位置为子module的根目录
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
set(BREAKPAD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/src/main/include)
include_directories(${BREAKPAD_ROOT} ${BREAKPAD_ROOT}/common/android/include)
#设置NDK生成的so包路径:jniLibs/
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})
set(ENABLE_INPROCESS ON)
set(ENABLE_OUTOFPROCESS ON)
set(ENABLE_LIBCORKSCREW ON)
set(ENABLE_LIBUNWIND ON)
set(ENABLE_LIBUNWINDSTACK ON)
set(ENABLE_CXXABI ON)
set(ENABLE_STACKSCAN ON)
if (${ENABLE_INPROCESS})
add_definitions(-DENABLE_INPROCESS)
endif ()
if (${ENABLE_OUTOFPROCESS})
add_definitions(-DENABLE_OUTOFPROCESS)
endif ()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=implicit-function-declaration")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 ")
file(GLOB BREAKPAD_SOURCES_COMMON
${BREAKPAD_ROOT}/../cpp/breakpad.cpp
${BREAKPAD_ROOT}/client/linux/crash_generation/crash_generation_client.cc
${BREAKPAD_ROOT}/client/linux/dump_writer_common/thread_info.cc
${BREAKPAD_ROOT}/client/linux/dump_writer_common/ucontext_reader.cc
${BREAKPAD_ROOT}/client/linux/handler/exception_handler.cc
${BREAKPAD_ROOT}/client/linux/handler/minidump_descriptor.cc
${BREAKPAD_ROOT}/client/linux/log/log.cc
${BREAKPAD_ROOT}/client/linux/microdump_writer/microdump_writer.cc
${BREAKPAD_ROOT}/client/linux/minidump_writer/linux_dumper.cc
${BREAKPAD_ROOT}/client/linux/minidump_writer/linux_ptrace_dumper.cc
${BREAKPAD_ROOT}/client/linux/minidump_writer/minidump_writer.cc
${BREAKPAD_ROOT}/client/minidump_file_writer.cc
${BREAKPAD_ROOT}/common/convert_UTF.cc
${BREAKPAD_ROOT}/common/md5.cc
${BREAKPAD_ROOT}/common/string_conversion.cc
${BREAKPAD_ROOT}/common/linux/elfutils.cc
${BREAKPAD_ROOT}/common/linux/file_id.cc
${BREAKPAD_ROOT}/common/linux/guid_creator.cc
${BREAKPAD_ROOT}/common/linux/linux_libc_support.cc
${BREAKPAD_ROOT}/common/linux/memory_mapped_file.cc
${BREAKPAD_ROOT}/common/linux/safe_readlink.cc
)
file(GLOB BREAKPAD_ASM_SOURCE ${BREAKPAD_ROOT}/common/linux/breakpad_getcontext.S
)
set_source_files_properties(${BREAKPAD_ASM_SOURCE} PROPERTIES LANGUAGE C)
add_library( # Sets the name of the library.
breakpad-core
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${BREAKPAD_SOURCES_COMMON} ${BREAKPAD_ASM_SOURCE})
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
breakpad-core
# Links the target library to the log library
# included in the NDK.
${log-lib} )
上述配置会将so打包为 名为 libbreakpad-core.so
- 主工程中引入子module,并进行初始化
- 在主工程中,我们编写一个必定crash的so
#include <stdio.h>
#include <jni.h>
/**
* 引起 crash
*/
void Crash() {
volatile int *a = (int *) (NULL);
*a = 1;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_kl_crash_Crash_crash(JNIEnv *env, jobject obj) {
Crash();
}
- 在主工程中调用crash的代码,我们发下app crash 了,并且在logcat中打印两行日志
我们通过android studio 的file explorer 找到 /data/data/com.kl.breakpadtestdemo/files/crashDump文件夹下的 dmp 文件,导出到电脑
- cd到 我们的breakpad 工程的src 目录,我们利用里面的processor 目录下的脚本进行解析dmp文件
./minidump_stackwalk ***.dmp > crashLog.txt
打开文件后可以看到一个详细的 crash 日志,如下
Operating system: Android
0.0.0 Linux 4.9.186-perf-g78b7d9a #1 SMP PREEMPT Wed Aug 5 00:12:02 CST 2020 armv8l
CPU: arm
ARMv1 Qualcomm part(0x51008030) features: half,thumb,fastmult,vfpv2,edsp,neon,vfpv3,tls,vfpv4,idiva,idivt
8 CPUs
GPU: UNKNOWN
Crash reason: SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed)
0 libcrash-lib.so + 0x5b2
r0 = 0x00000000 r1 = 0x00000001 r2 = 0xffe8805c r3 = 0xea6af3c0
r4 = 0xddbcb5b4 r5 = 0xbf150ba0 r6 = 0x00000002 r7 = 0xffe88038
r8 = 0x00000000 r9 = 0xea7f1e00 r10 = 0xffe88060 r12 = 0xbe343fe8
fp = 0xea7f1e00 sp = 0xffe88024 lr = 0xbe3415cf pc = 0xbe3415b2
Found by: given as instruction pointer in context
1 libart.so + 0xdc519
sp = 0xffe88040 pc = 0xe5d4e51b
Found by: stack scanning
2 libart-compiler.so + 0x234ccb
sp = 0xffe88070 pc = 0xdb9c7ccd
Found by: stack scanning
3 libart-compiler.so + 0x234ccb
sp = 0xffe8808c pc = 0xdb9c7ccd
Found by: stack scanning
4 libart.so + 0xfc2bd
sp = 0xffe880b4 pc = 0xe5d6e2bf
Found by: stack scanning
5 base.apk!classes2.dex] + 0x4db9e
我们可以发现日志在 Thread0 中有崩溃,并且指向了我们自己写的libcrash-lib.so 的0x5b2
我们有如下这种方式进行解析到问题的根源:
- 进入本地ndk目录下的/toolchains目录下
我们发下有如下的各个目录,分别对应的是各不同平台下的so的符号解析工具链
我们这里demo已经强制使用了armabi-v7a ,所以我们进入到 arm-linux-androideabi-4.9
目录下
cd /toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
arm-linux-androideabi-addr2line -f -C -e xxxx/armeabi-v7a/libcrash-lib.so 0x5b2
我们可以看到如下的输出:
Crash()
/Users/xx/Code/MyBreakPadDemo/app/.cxx/cmake/debug/armeabi-v7a/../../../../src/main/cpp/crash.cpp:10
也就是指向了 crash.cpp的第10行,也就是
我们也就可以找到了崩溃的源头
最后附上我的测试demo,随意下载使用。
总结
在Android平台上使用Breakpad进行native崩溃定位的整个流程就结束了,显然Breakpad优点很明显,首先它具有跨平台的特性,其次它也是google颇为得意的一款开源工具集,权威性不言而喻。缺点也很明显,从集成目标项目,到最后的崩溃日志分析,整个过程的步骤比较复杂,而且Breakpad的代码体量也比较大。如果把Breakpad集成到我们自己的项目中又会出现一些新的问题,比如目前我们软终端Android项目中的so库大大小小将近二十个,发生崩溃时,如果我们无法定位究竟是哪一个so引发的问题,那就比较头大了。最坏情况下,要对所有的so进行一番分析,所以对待native崩溃还是要结合其他手段一起定位,分析native crash并不是一件容易的事情。
另外:当你使用breakpad的时候,和bugly就不能同时使用,只能有一个获取到对应的堆栈。请注意
参考资料
Google Breakpad 学习笔记
demo
mirror breakpad
Android使用google breakpad捕获分析native cash