监控Java层和JNI Native层Crash

原文
源码

监控Java层和JNI Native层Crash,分析.so库报错的堆栈信息

Crash(应用崩溃)是由于代码异常而导致 App 非正常退出,导致应用程序无法继续使用,所有工作都停止的现象。发生 Crash 后需要重新启动应用(有些情况会自动重启),而且不管应用在开发阶段做得多么优秀,也无法避免 Crash 发生,特别是在Android 系统中,系统碎片化严重、各 ROM 之间的差异,甚至系统Bug,都可能会导致Crash的发生。在 Android 应用中发生的 Crash 有两种类型,Java 层的 Crash 和 Native 层 Crash。这两种Crash 的监控和获取堆栈信息有所不同。

1、Java Crash

Java的Crash监控非常简单,Java中的Thread定义了一个接口: UncaughtExceptionHandler ;用于处理未捕获的异常导致线程的终止(注意:catch了的是捕获不到的),当我们的应用crash的时候,就会走UncaughtExceptionHandler.uncaughtException ,在该方法中可以获取到异常的信息,我们通过Thread.setDefaultUncaughtExceptionHandler 该方法来设置线程的默认异常处理器,我们可以将异常信息保存到本地或者是上传到服务器,方便我们快速的定位问题。

public class JavaCrashHandler implements Thread.UncaughtExceptionHandler {

    private static final String FILE_NAME_SUFFIX = ".log";
    private static Thread.UncaughtExceptionHandler mDefaultCrashHandler;
    private static Context mContext;

    private JavaCrashHandler() {
    }

    public static void init(@NonNull Context context) {
        // 默认为:RuntimeInit#KillApplicationHandler
        mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
        mContext = context.getApplicationContext();
        Thread.setDefaultUncaughtExceptionHandler(new JavaCrashHandler());
    }

    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
        try {
            // 自行处理:保存本地
            File file = dealException(thread, throwable);
            // 上传服务器
            // ......
        } catch (Exception e1) {
            e1.printStackTrace();
        } finally {
            // 交给系统默认程序处理
            if (mDefaultCrashHandler != null) {
                mDefaultCrashHandler.uncaughtException(thread, throwable);
            }
        }
    }

    private File dealException(Thread thread, Throwable throwable) {
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        File crashFolder = new File(mContext.getExternalCacheDir().getAbsoluteFile(), CrashMonitor.DEFAULT_JAVA_CRASH_FOLDER_NAME);
        if (!crashFolder.exists()) {
            crashFolder.mkdirs();
        }
        File crashFile = new File(crashFolder, time + FILE_NAME_SUFFIX);
        try {
            // 往文件中写入数据
            PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)));
            pw.println(time);
            pw.println(thread);
            pw.println(getPhoneInfo());
            throwable.printStackTrace(pw);
            // 写入crash堆栈
            pw.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return crashFile;
    }

    private String getPhoneInfo() {
        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = null;
        StringBuilder sb = new StringBuilder();

        try {
            pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);

            // App版本
            sb.append("App Version: ");
            sb.append(pi.versionName);
            sb.append("_");
            sb.append(pi.versionCode + "\n");
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        // Android版本号
        sb.append("OS Version: ");
        sb.append(Build.VERSION.RELEASE);
        sb.append("_");
        sb.append(Build.VERSION.SDK_INT + "\n");

        // 手机制造商
        sb.append("Vendor: ");
        sb.append(Build.MANUFACTURER + "\n");

        // 手机型号
        sb.append("Model: ");
        sb.append(Build.MODEL + "\n");

        // CPU架构
        sb.append("CPU: ");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            sb.append(Arrays.toString(Build.SUPPORTED_ABIS));
        } else {
            sb.append(Build.CPU_ABI);
        }

        return sb.toString();
    }
}

2、NDK Crash (Native、JNI)

相对于Java的Crash,NDK的错误无疑更加让人头疼,特别是对初学NDK的同学,不说监控,就算是错误堆栈都不知道怎么看。

2.1、Linux信号机制

信号机制是Linux进程间通信的一种重要方式,Linux信号一方面用于正常的进程间通信和同步,另一方面它还负责监控系统异常及中断。当应用程序运行异常时,Linux内核将产生错误信号并通知当前进程。当前进程在接收到该错误信号后,可以有三种不同的处理方式。

  • 忽略该信号;
  • 捕捉该信号并执行对应的信号处理函数(信号处理程序);
  • 执行该信号的缺省操作(如终止进程);

当Linux应用程序在执行时发生严重错误,一般会导致程序崩溃。其中,Linux专门提供了一类crash信号,在程序接收到此类信号时,缺省操作是将崩溃的现场信息记录到核心文件,然后终止进程。
常见崩溃信号列表:

信号 描述
SIGSEGV 内存引用无效。
SIGBUS 访问内存对象的未定义部分。
SIGFPE 算术运算错误,除以零。
SIGILL 非法指令,如执行垃圾或特权指令。
SIGSYS 糟糕的系统调用。
SIGXCPU 超过CPU时间限制。
SIGXFSZ 文件大小限制。

一般的出现崩溃信号,Android系统默认缺省操作是直接退出我们的程序。但是系统允许我们给某一个进程的某一个特定信号注册一个相应的处理函数(signal),即对该信号的默认处理动作进行修改。因此NDK Crash的监控可以采用这种信号机制,捕获崩溃信号执行我们自己的信号处理函数从而捕获NDK Crash。

2.2、墓碑文件(tombstones)

Android本机程序本质上就是一个Linux程序,当它在执行时发生严重错误,也会导致程序崩溃,然后产生一个记录崩溃的现场信息的文件,而这个文件在Android系统中就是 tombstones 墓碑文件。

此处了解即可,普通应用无权限读取墓碑文件,墓碑文件位于路径/data/tombstones/下。解析墓碑文件与后面的breakpad都可使用 addr2line 工具。

2.3、BreakPad

Google breakpad是一个跨平台的崩溃转储和分析框架和工具集合,其开源地址是:https://github.com/google/breakpad。breakpad在Linux中的实现就是借助了Linux信号捕获机制实现的。因为其实现为C++,因此在Android中使用,必须借助NDK工具。

2.4、Native Crash监控

将Breakpad源码下载解压,首先查看README.ANDROID文件。

README.ANDROID文件所在
README.ANDROID内容

按照文档中的介绍,如果我们使用Android.mk 非常简单就能够引入到我们工程中,但是目前NDK默认的构建工具为:CMake,因此我们做一次移植。查看android/google_breakpad/Android.mk

include $(CLEAR_VARS)

# 最后编译出 libbreakpad_client.a
LOCAL_MODULE := breakpad_client

# 指定c++源文件后缀名
LOCAL_CPP_EXTENSION := .cc

# 强制构建系统以 32 位 arm 模式生成模块的对象文件
LOCAL_ARM_MODE := arm

# 需要编译的源码
LOCAL_SRC_FILES := \
    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/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

# 导入头文件
LOCAL_C_INCLUDES        := $(LOCAL_PATH)/src/common/android/include \
                           $(LOCAL_PATH)/src \
                           $(LSS_PATH) # 注意这个目录

# 导出头文件
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES)

# 使用android ndk中的日志库log
LOCAL_EXPORT_LDLIBS     := -llog

# 编译static静态库-》类似java的jar包
include $(BUILD_STATIC_LIBRARY)

注意:mk文件中 LOCAL_C_INCLUDES 的 LSS_PATH 是个坑

对照Android.mk文件,我们在自己项目的cpp(工程中C/C++源码)目录下创建breakpad目录,并将下载的breakpad源码根目录下的src目录全部复制到我们的项目中:

breakpad目录结构

接下来在breakpad目录下创建CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.4.1)

#引入breakpad的头文件(api的定义)
include_directories(breakpad/src breakpad/src/common/android/include)

#引入breakpad的cmakelist,执行并生成libbreakpad.a (api的实现,类似java的jar包)
add_subdirectory(breakpad)

add_library(crash_monitor SHARED native_crash_monitor.cpp)

target_link_libraries(crash_monitor
        breakpad #引入breakpad的库文件(api的实现)
        log)

在cpp目录下(breakpad同级)还有一个CMakeList.txt文件,它的内容是:

cmake_minimum_required(VERSION 3.4.1)

#引入breakpad的头文件(api的定义)
include_directories(breakpad/src breakpad/src/common/android/include)

#引入breakpad的cmakelist,执行并生成libbreakpad.a (api的实现,类似java的jar包)
add_subdirectory(breakpad)

add_library(crash_monitor SHARED native_crash_monitor.cpp)

target_link_libraries(crash_monitor
        breakpad #引入breakpad的库文件(api的实现)
        log)

此时执行编译,会在 #include "third_party/lss/linux_syscall_support.h" 报错,无法找到头文件。此文件从:https://chromium.googlesource.com/external/linux-syscall-support/+/refs/heads/master 下载(需要翻墙)放到工程对应目录即可。

也可从附件链接Demo获取

native_crash_monitor.cpp 源文件中的实现为:

#include <jni.h>
#include <android/log.h>
#include <client/linux/handler/minidump_descriptor.h>
#include <client/linux/handler/exception_handler.h>

extern "C"
JNIEXPORT void JNICALL
Java_com_aaa_crashmonitor_CrashMonitor_testNativeCrash(JNIEnv *env, jclass clazz) {
    __android_log_print(ANDROID_LOG_ERROR, "testNativeCrash", "往指向空地址的指针里存值");
    int *p = NULL;
    *p = 10;
}

bool
DumpCallback(const google_breakpad::MinidumpDescriptor &descriptor, void *context, bool succeeded) {
    __android_log_print(ANDROID_LOG_ERROR, "ndk_crash", "Dump path: %s", descriptor.path());
    // 如果回调返回true,Breakpad将把异常视为已完全处理,禁止任何其他处理程序收到异常通知。
    // 如果回调返回false,Breakpad会将异常视为未处理,并允许其他处理程序处理它。
    return false;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_aaa_crashmonitor_CrashMonitor_initNativeCrashMonitor(JNIEnv *env, jclass clazz,
                                                              jstring path_) {
    const char *path = env->GetStringUTFChars(path_, 0);
    __android_log_print(ANDROID_LOG_DEBUG, "initNativeCrashMonitor", "crash堆栈保存路径=%s ", path);
    // 开启crash监控
    google_breakpad::MinidumpDescriptor descriptor(path);
    static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback, NULL, true, -1);
    __android_log_print(ANDROID_LOG_DEBUG, "initNativeCrashMonitor", "breakpad已开启");
    env->ReleaseStringUTFChars(path_, path);
}

注意JNI方法的方法名对应了java类,创建Java源文件:

public class CrashMonitor {

    public static final String DEFAULT_JAVA_CRASH_FOLDER_NAME = "java_crash";
    public static final String DEFAULT_NATIVE_CRASH_FOLDER_NAME = "native_crash";

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

    public static void initAll(Context context) {
        initJavaCrashMonitor(context);
        initNativeCrashMonitor(context);
    }

    public static void initJavaCrashMonitor(Context context) {
        JavaCrashHandler.init(context.getApplicationContext());
    }

    public static void initNativeCrashMonitor(Context context) {
        Context appContext = context.getApplicationContext();
        File crashFile = new File(appContext.getExternalCacheDir(), DEFAULT_NATIVE_CRASH_FOLDER_NAME);
        if (!crashFile.exists()) {
            crashFile.mkdirs();
        }
        initNativeCrashMonitor(crashFile.getAbsolutePath());
    }

    public static native void initNativeCrashMonitor(String path);

    public static native void testNativeCrash();

    public static void testJavaCrash() {
        int a = 1 / 0;
    }
}
整体目录结构

此时,如果出现NDK Crash,会在我们指定的目录:/sdcard/Android/data/com.aaa.crashmonitor/cache/native_crash 下生成NDK Crash信息文件。

生成的crash文件

2.5、Native Crash分析

采集到的Crash信息记录在minidump文件中。minidump是由微软开发的用于崩溃上传的文件格式。我们可以将此文件上传到服务器完成上报,但是此文件没有可读性可言,要将文件解析为可读的崩溃堆栈需要按照breakpad文档编译 minidump_stackwalk 工具,而Windows系统编译个人不会。不过好在,无论你是 Mac、windows还是ubuntu在 Android Studio 的安装目录下的 bin\lldb\bin 里面就存在一个对应平台的 minidump_stackwalk 。

使用这里的工具执行:

minidump_stackwalk xxxx.dump > native_crash.txt

打开 native_crash.txt 内容为:

Operating system: Android
                  0.0.0 Linux 4.14.112+ #1 SMP PREEMPT Fri Sep 13 21:20:42 UTC 2019 i686
CPU: x86 // abi类型
     GenuineIntel family 6 model 31 stepping 1
     4 CPUs

GPU: UNKNOWN

Crash reason:  SIGSEGV // 内存引用无效信号
Crash address: 0x0
Process uptime: not available

Thread 0 (crashed) // crashed:出现crash的线程
 0  libcrash_monitor.so + 0x1e944 // crash的so与寄存器信息
    eip = 0xc7b26944   esp = 0xffbfdaf0   ebp = 0xffbfdb28   ebx = 0xc7ba04b8
    esi = 0xc7b8123c   edi = 0xc7b8124c   eax = 0x00000036   ecx = 0x00000000
    edx = 0x00000004   efl = 0x00010246
    Found by: given as instruction pointer in context
 1  libart.so + 0x144f68
    eip = 0xed51af68   esp = 0xffbfdb30   ebp = 0xffbfdb50
    Found by: previous frame's frame pointer

接下来使用 Android NDK 里面提供的 addr2line 工具将寄存器地址转换为对应符号。addr2line 要用和自己 so 的 ABI 匹配的目录,同时需要使用有符号信息的so(一般debug的就有)。

因为我使用的是模拟器x86架构,因此addr2line位于:Android\Sdk\ndk\20.0.5594570\toolchains\x86-4.9\prebuilt\windows-x86_64\bin\i686-linux-android-addr2line.exe

i686-linux-android-addr2line.exe -f -C -e libcrash_monitor.so 0x1e934
分析定位出so库中出现crash的行信息.png
so库中c++文件出现crash代码.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,873评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,483评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,525评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,595评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,018评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,958评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,118评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,873评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,643评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,813评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,293评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,615评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,306评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,170评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,968评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,107评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,894评论 2 278