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 的,通常体积会比较大。

Android 平台 Native Crash 问题分析与定位_第1张图片

通常 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。

Android 平台 Native Crash 问题分析与定位_第2张图片

Android 平台 Native Crash 问题分析与定位_第3张图片

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 示例。Android 平台 Native Crash 问题分析与定位_第4张图片

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 等异常信号 获取崩溃事件

工作原理示意图 Android 平台 Native Crash 问题分析与定位_第5张图片

图片右上角是一个完整的应用程序,它包含了三部分即程序代码、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 目录下

Android 平台 Native Crash 问题分析与定位_第6张图片

然后在 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 平台 Native Crash 问题分析与定位_第7张图片

你可能感兴趣的:(Android技术,android,android,studio,java)