Android 平台 Native Crash 问题分析与定位

一 Native Crash 简介

Native Crash 是发生在 Android 系统中 C/C++ 层面的 Crash,具体可参考: # Android 平台 Native Crash 捕获原理详解

二 Native C/C++ Libraries 简介

Android 开发中通常是将 Native 层代码打包为.so格式的动态库文件,然后供 Java 层调用,.so库文件通常有以下三种来源:

  • Android 系统自带的核心组件和服务,如多媒体库、OpenGL ES 图形库等
  • 引入的第三方库
  • 开发者自行编译生成的动态库

2.1 .so文件组成

一个完整的 .so 文件由 C/C++代码和一些 debug 信息组成,这些 debug 信息会记录 .so中所有方法的对照表,就是方法名和其偏移地址的对应表,也叫做符号表 symbolic 信息,这种 .so被称为未 strip 的,通常体积会比较大。

通常 release 的.so都是需要经过 strip 操作,strip 之后的.so中的 debug 信息会被剥离,整个 so 的体积也会缩小许多。

可以简单将这个 debug 信息理解为 Java 代码混淆中的 mapping 文件,只有拥有这个 mapping 文件才能进行堆栈分析。如果堆栈信息丢了,基本上堆栈无法还原,问题也无法解决。

所以,这些 debug 信息尤为重要,是我们分析 Native Crash 问题的关键信息,那么我们在编译 .so 时 候务必保留一份未被 strip 的.so或者剥离后的符号表信息,以供后面问题分析。

2.2 查看 so 状态

也可以通过命令行来查看.so的状态,Linux 下使用 file 命令即可,在命令返回值里面可以查看到.so的一 些基本信息。

如下代码所示,stripped 代表是没有 debug 信息的.so,with debug_info, not stripped 代表携带 debug 信息的.so

file libbreakpad-core-s.so
libbreakpad-core-s.so: *******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, stripped
file libbreakpad-core.so
libbreakpad-core.so: ******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, with debug_info, not stripped

2.3 获取 strip 和未被 strip 的 so

目前 Android Studio 无论是使用 mk 或者 Cmake 编译的方式都会同时输出 strip 和未 strip 的 so,如下图是 Cmake 编译 so 产生的两个对应的 so。

strip 之前的 so 路径:{project}/app/build/intermediates/merged_native_libs

strip 之后的 so 路径:{project}/app/build/intermediates/stripped_native_libs

三 Native Crash 捕获与解析

3.1 通过 DropBox 日志解析

Android Dropbox 是 Android 在 Froyo(API level 8) 引入的用来持续化存储系统数据的机制。主要用于记录 Android 运行过程中, 内核, 系统进程, 用户进程等出现严重问题时的 log, 可以认为这是一个可持续存储的系统级别的 logcat。

相关文件记录存储目录:/data/system/dropbox

只需要将 DropBox 的日志获取到即可进行分析解决,下面贴上一份 Log 示例。

DropBox 中的 Tombstone 文件显示,Native Crash 发生在动态库 libnativedemo.so 中,具体的方法和行数可以用 Android/SDK/NDK 提供的工具 linux-android-addr2line 来进一步定位。

addr2line 工具通常在 ndk 目录下,例如:

${SDK Path}/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line

然后使用命令行,既可将偏移地址转换为 crash 方法和行数

arm-linux-androideabi-addr2line [option(s)] [addr(s)]

简单来说就是 arm-linux-androideabi-addr2line + 可选项 + 异常地址

[option(s)] 介绍
@ 从文件中读取 options
-a 在结果中显示地址 addr
-b 设置二进制文件的格式
-e 设置输入文件(常用:选项后面需要跟报错的共享库,用于 addr2line 程序分析)
-i unwind inline function
-j Read section-relative offsets instead of addresses
-p 让输出更易读
-s 在输出中,剥离文件夹名称
-f 显示函数名称
-C (大写的) 将输出的函数名 demangle
-h 输出帮助
-v 输出版本信息

使用 addr2line 进行解析,结果可以看到,Native Crash 发生在文件 native-lib.cpp17 行的 Crash() 方法

结合代码分析,在 Crash() 中,对空指针 *a 进行了赋值操作,所以造成了 crash。

#include 
#include 

extern "C" JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

/**
 * 引起 crash
 */
void Crash() {
    volatile int *a = (int *) (NULL);
    *a = 1;
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
    Crash();
}

通过读取 DropBox 获得 crash log -> addr2line 解析偏移地址的方法确实可以定位到 native crash 发生的现场,但是 DropBox 只有系统应用能访问,非系统应用拿不到日志。对于非系统应用,可以使用 google 提供的开源工具 BreakPad 进行监测分析。

3.2 通过 BreakPad 捕获解析

3.2.1 breakpad 简介

BreakPad 是 Google 开发的一个跨平台 C/C++ dump捕获开源库,崩溃文件使用微软的 minidump格式存储,也支持发送这个 dump 文件到你的服务器,breakpad 可以在程序崩溃时触发 dump 写入操作,也可以在没有触发 dump 时主动写 dump 文件。breakpad 支持 windows、linux、macos、android、ios 等。目前已有 Google Chrome, Firefox, Google Picasa, Camino, Google Earth 等项目使用。

3.2.2 实现原理

在不同平台下使用平台特有的函数以及方式实现异常捕获:

Windows:通过 SetUnhandledExceptionFilter()设置崩溃回掉函数

Max OS:监听 Mach Exception Port 获取崩溃事件

Linux:监听 SIGILL SIGSEGV 等异常信号 获取崩溃事件

工作原理示意图

图片右上角是一个完整的应用程序,它包含了三部分即程序代码、Breakpad Client(即 brekapad 提供出来的静态库),调式信息

  • Build System中 breakpad 的 symbol 生成工具借助应用层序中的 Debugging Information 这一部分生成一个 Google 自己的符号文件,最终在发布应用层序的时候使用 strip 将调式信息去除

  • User's System中运行的应用程序是通过 strip 去除了调式信息的,若应用程序发生 Crash,Breakpad client 就会写 minidump 文件到指定目录,也可以将产生的 minidump 文件发送到远端服务器即 Crash Colletcor。

  • Crash Collector就可以利用 Build System 中产生的 symol 文件和 User's System 中上报的 minidump 文件生成用户可读的 stack trace

3.2.3 使用示例

获取 breakpad 源码

github.com/google/brea…

执行安装 breakpad

1. cd breakpad 目录
2. 直接命令窗口输入:

./configure && make

移植 Breakpad 到客户端程序

breakpad 源码导入应用程序 cpp 目录下

然后在 breakpad 中创建 CMakeLists.txt

cmake_minimum_required(VERSION 3.18.1)

#导入头文件
include_directories(src src/common/android/include)
#支持汇编文件的编译
enable_language(ASM)
#源文件编译为静态库
add_library(breakpad STATIC
        src/client/linux/crash_generation/crash_generation_client.cc
        src/client/linux/dump_writer_common/thread_info.cc
        src/client/linux/dump_writer_common/ucontext_reader.cc
        src/client/linux/handler/exception_handler.cc
        src/client/linux/handler/minidump_descriptor.cc
        src/client/linux/log/log.cc
        src/client/linux/microdump_writer/microdump_writer.cc
        src/client/linux/minidump_writer/linux_dumper.cc
        src/client/linux/minidump_writer/linux_ptrace_dumper.cc
        src/client/linux/minidump_writer/minidump_writer.cc
        src/client/linux/minidump_writer/pe_file.cc
        src/client/minidump_file_writer.cc
        src/common/convert_UTF.cc
        src/common/md5.cc
        src/common/string_conversion.cc
        src/common/linux/breakpad_getcontext.S
        src/common/linux/elfutils.cc
        src/common/linux/file_id.cc
        src/common/linux/guid_creator.cc
        src/common/linux/linux_libc_support.cc
        src/common/linux/memory_mapped_file.cc
        src/common/linux/safe_readlink.cc)
#导入相关的库
target_link_libraries(breakpad log)

breakpad 中的 CMakeLists.txt 创建完成后,还需要在 cpp 目录下的 CMakeLists.txt 中进行配置,将刚刚创建的 CMakeLists.txt 引入进去

cmake_minimum_required(VERSION 3.18.1)

#引入头文件
include_directories(breakpad/src breakpad/src/common/android/include)

add_library(nativecrash SHARED nativecrashlib.cpp)

#添加子目录,会自动查找这个目录下的 CMakeList
add_subdirectory(breakpad)

target_link_libraries(nativecrash log breakpad)

breakpad 初始化

然后在自己项目的 native 文件中对 breakpad 进行初始化,如下

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

/**
 * 引起 crash
 */
void Crash() {
    volatile int *a = (int *) (NULL);
    *a = 1;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
    Crash();
}

//回调函数
bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
                  void* context,
                  bool succeeded) {
    printf("Dump path: %s\n", descriptor.path());
    return false;
}

//breakpad 初始化
extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_initNative(JNIEnv *env, jclass clazz, 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);
}

Java 层代码

Java 层传入 Crash dump 文件的保存路径,用于崩溃时文件的生成

package com.elijah.nativedemo;

import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.io.File;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("nativedemo");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init(this);
        findViewById(R.id.crash)
                .setOnClickListener(
                        new View.OnClickListener() {
                            @Override
                            public void onClick(View view) {
                                nativeCrash();
                            }
                        });
    }

    public static void init(Context context){
        Context applicationContext = context.getApplicationContext();
        File file = new File(applicationContext.getExternalCacheDir(),"native_crash");
        if(!file.exists()){
            file.mkdirs();
        }
        initNative(file.getAbsolutePath());
    }

    /**
     * 模拟崩溃
     */
    public static native void nativeCrash();

    /**
     * 初始化 breakpad
     * @param path
     */
    private static native void initNative(String path);
}

捕获 Crash,解析 dump

Native Crash 产生后,breakpad 会捕获 crash 信息,生成后缀为.dmp的 dump 文件到指定目录下。

.dmp 格式的文件通常无法查看,需要解析工具对这个文件进行解析。解析工具在步骤“执行安装 breakpad”中就已经生成在 breakpad/src/processor目录下,名为 minidump_stackwalk

输入如下指令即可解析 dump 文件

./minidump_stackwalk my.dump > crash.txt

生成的 crash.txt 如下图所示,关键代码是红框的部分,Thread 0 后面有一个 crashed 标识,说明这里是发生崩溃的线程,而下面就是崩溃的文件以及内存地址,使用 3.1 中介绍的 addr2line 工具进行解析即可得到问题方法与行号

参考文献

Android NativeCrash 捕获与解析

Android---Native层崩溃的监听工具BreakPad

作者:话唠扇贝
链接:https://juejin.cn/post/7124689738811834382
更多资讯可关注【公众号---敲代码的小竹】

你可能感兴趣的:(Android 平台 Native Crash 问题分析与定位)