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描述例子
SIGABRT6程序发生错误或者调用了abort很多C的库函数,如果发现异常会调用abort,如strlen
SIGBUS10,7,10不存在的物理地址硬件错误更多的是因为硬件或者系统引起的
SIGFPE8浮点数运算错误如除0,余0,整型溢出
SIGILL4非法指令损坏的可执行文件或者代码区损坏
SIGSEGV11段地址错误空指针,访问不存在的地址空间,访问内核区,写只读空间,栈溢出,数组越界,野指针
SIGSTKFLT16
SIGPIPE13管道错误,往没有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,随意下载使用。

github_breakpaddemo

总结

在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

最后修改:2021 年 01 月 11 日 15 : 22

发表评论

  • 小星星变奏曲 - 莫扎特
  • Moon River - Audrey Hepburn