xCrash捕获Native异常(一)

谁都会走 提交于 2019-12-30 11:37:44

Native异常

android 开发过程中有时候需要使用JNI的方式调用C/C++的库。因此在调试的过程如果发现崩溃异常,如果能够获取C/C++ 的异常堆栈,则可以方便的确定哪一行代码出现了问题,方便快速的定位问题。
在捕获Native异常中,原理上面基本是采用linux的信号机制。

linux信号机制

关于Unix-like系统的信号机制可以参见《深入Linux内核》第4章 中断和异常 ;第11章 信号。
关于信号和异常介绍比较好的博客有:
https://blog.csdn.net/ypt523/article/details/80290208。感谢博主的无私贡献。
Linux 信号相关编程,进程编程,需要查询相关资料,本文不进行介绍。

xCrash捕获Native异常配置

与Native异常相关的配置属性有

public final class XCrash{
	...
	 public static class InitParameters {
		 // anr异常处理器,默认为true,如果为false不捕获anr异常
        boolean        enableAnrHandler     = true;
        //  是否恢复捕获anr异常。默认为true
        boolean        anrRethrow           = true;
        // 是否设置anr的状态标志给进程状态(具体参见源码中的注释)
        boolean        anrCheckProcessState = true;
        // anr日志最大保留文件数量
        int            anrLogCountMax       = 10;
        // 执行命令 logcat -b system 输出的日志行数 
        int            anrLogcatSystemLines = 50;
        // 执行命令 logcat -b event 输出的日志行数 
        int            anrLogcatEventsLines = 50;
        // 执行命令 logcat -b maint输出的日志行数 
        int            anrLogcatMainLines   = 200;
        // 是否输出app进程的下打开的文件描述符
        boolean        anrDumpFds           = true;
        // 发生anr异常的应用回调
        ICrashCallback anrCallback          = null;
	}
}

xCrash的Native异常的初始化接口

public final class XCrash{
	// 默认初始化接口  默认捕获native异常
	    public static int init(Context ctx) {
        return init(ctx, null);
    }
    // 自定义配置初始化接口
    public static synchronized int init(Context ctx, InitParameters params){
    	// Native 异常处理器初始化
    	        //init native crash handler / ANR handler (API level >= 21)
        int r = Errno.OK;
        if (params.enableNativeCrashHandler || (params.enableAnrHandler && Build.VERSION.SDK_INT >= 21)) {
            r = NativeHandler.getInstance().initialize(
                ctx, // context上下文
                params.libLoader,// so库加载路径
                appId, //appID
                params.appVersion,// 应用版本号
                params.logDir, // 日志输出路径
                params.enableNativeCrashHandler,
                params.nativeRethrow,
                params.nativeLogcatSystemLines,
                params.nativeLogcatEventsLines,
                params.nativeLogcatMainLines,
                params.nativeDumpElfHash,
                params.nativeDumpMap,
                params.nativeDumpFds,
                params.nativeDumpAllThreads,
                params.nativeDumpAllThreadsCountMax,
                params.nativeDumpAllThreadsWhiteList,
                params.nativeCallback,
                // 以下配置与ANR相关
                params.enableAnrHandler && Build.VERSION.SDK_INT >= 21,
                params.anrRethrow,
                params.anrCheckProcessState,
                params.anrLogcatSystemLines,
                params.anrLogcatEventsLines,
                params.anrLogcatMainLines,
                params.anrDumpFds,
                params.anrCallback);
        }
    }
}

xCrash的Native异常处理

类NativeHandler是处理Native异常,NativeHandler为单例模式,源码参见如下。

class NativeHandler {
	...
	private static final NativeHandler instance = new NativeHandler();
	    private NativeHandler() {
    }

    static NativeHandler getInstance() {
        return instance;
    }
}

NativeHandler 初始化接口

NativeHandler 中最重要的是初始化接口,初始化接口主要功能有:

  1. 加载so动态库
  2. 初始化C/C++库,用于捕获Native异常。
int initialize(...)
{
	// 加载libxcrash.so
	if (libLoader == null) {
            try {
                System.loadLibrary("xcrash");
            } catch (Throwable e) {
                XCrash.getLogger().e(Util.TAG, "NativeHandler System.loadLibrary failed", e);
                return Errno.LOAD_LIBRARY_FAILED;
            }
        } else {
            try {
            	// 加载指定路径下面的
                libLoader.loadLibrary("xcrash");
            } catch (Throwable e) {
                XCrash.getLogger().e(Util.TAG, "NativeHandler ILibLoader.loadLibrary failed", e);
                return Errno.LOAD_LIBRARY_FAILED;
            }
        }
        // 初始化接口
        int r = nativeInit(...)
}
// JNI接口,初始化接口
    private static native int nativeInit(
            int apiLevel,
            String osVersion,
            String abiList,
            String manufacturer,
            String brand,
            String model,
            String buildFingerprint,
            String appId,
            String appVersion,
            String appLibDir,
            String logDir,
            boolean crashEnable,
            boolean crashRethrow,
            int crashLogcatSystemLines,
            int crashLogcatEventsLines,
            int crashLogcatMainLines,
            boolean crashDumpElfHash,
            boolean crashDumpMap,
            boolean crashDumpFds,
            boolean crashDumpAllThreads,
            int crashDumpAllThreadsCountMax,
            String[] crashDumpAllThreadsWhiteList,
            boolean traceEnable,
            boolean traceRethrow,
            int traceLogcatSystemLines,
            int traceLogcatEventsLines,
            int traceLogcatMainLines,
            boolean traceDumpFds);
	// JNI 接口,通知natvie异常信息。
    private static native void nativeNotifyJavaCrashed();

xCrash C/C++代码

xCrash工程目录下面有java文件夹和native文件夹,natvie文件夹内的源码用于捕获native异常。

native 代码结构

xCrash源码结构中除了Jave源码还有native源码,native源码的结构如下
在这里插入图片描述
从源码结构中可以分为:

  1. common文件夹,该文件夹内的源码为通用处理函数和方法,可以被libxcrash文件夹内和libxcrash_dumper的原文件调用。
  2. libxcrash文件夹,该文件夹内的源码编译成libxcrash.so
  3. libxcrash文件夹,改文件夹内的源码编译成libxcrash_dumper.so
  4. 三个脚本文件分为是:build.sh;clean.sh;install.sh

脚本文件

  1. build.sh脚本文件
    build.sh脚本文件非常简单,就是使用ndk编译libxcrash和/libxcrash_dumper
#!/bin/bash

ndk-build -C ./libxcrash/jni
ndk-build -C ./libxcrash_dumper/jni
  1. install.sh
    install.sh脚本也简单,主要完成的功能有:1,新建文件夹。2,将编译出来的so库拷贝到相应的路径下面。
#!/bin/bash

mkdir -p ../java/xcrash/xcrash_lib/src/main/jniLibs/armeabi
mkdir -p ../java/xcrash/xcrash_lib/src/main/jniLibs/armeabi-v7a
mkdir -p ../java/xcrash/xcrash_lib/src/main/jniLibs/arm64-v8a
mkdir -p ../java/xcrash/xcrash_lib/src/main/jniLibs/x86
mkdir -p ../java/xcrash/xcrash_lib/src/main/jniLibs/x86_64

cp -f ./libxcrash/libs/armeabi/libxcrash.so     ../java/xcrash/xcrash_lib/src/main/jniLibs/armeabi/libxcrash.so
cp -f ./libxcrash/libs/armeabi-v7a/libxcrash.so ../java/xcrash/xcrash_lib/src/main/jniLibs/armeabi-v7a/libxcrash.so
cp -f ./libxcrash/libs/arm64-v8a/libxcrash.so   ../java/xcrash/xcrash_lib/src/main/jniLibs/arm64-v8a/libxcrash.so
cp -f ./libxcrash/libs/x86/libxcrash.so         ../java/xcrash/xcrash_lib/src/main/jniLibs/x86/libxcrash.so
cp -f ./libxcrash/libs/x86_64/libxcrash.so      ../java/xcrash/xcrash_lib/src/main/jniLibs/x86_64/libxcrash.so

cp -f ./libxcrash_dumper/libs/armeabi/xcrash_dumper     ../java/xcrash/xcrash_lib/src/main/jniLibs/armeabi/libxcrash_dumper.so
cp -f ./libxcrash_dumper/libs/armeabi-v7a/xcrash_dumper ../java/xcrash/xcrash_lib/src/main/jniLibs/armeabi-v7a/libxcrash_dumper.so
cp -f ./libxcrash_dumper/libs/arm64-v8a/xcrash_dumper   ../java/xcrash/xcrash_lib/src/main/jniLibs/arm64-v8a/libxcrash_dumper.so
cp -f ./libxcrash_dumper/libs/x86/xcrash_dumper         ../java/xcrash/xcrash_lib/src/main/jniLibs/x86/libxcrash_dumper.so
cp -f ./libxcrash_dumper/libs/x86_64/xcrash_dumper      ../java/xcrash/xcrash_lib/src/main/jniLibs/x86_64/libxcrash_dumper.so
  1. clean.sh
    功能也非常简单,就是make clean的功能
#!/bin/bash
ndk-build -C ./libxcrash/jni clean
ndk-build -C ./libxcrash_dumper/jni clean

编译libxcrash.so

编译libxcrash.so是标准的使用ndk编译so的标准文件结构。在源码中需要Application.mk和Android.mk。
Application.mk源文件如下

APP_ABI      := armeabi armeabi-v7a arm64-v8a x86 x86_64
APP_PLATFORM := android-14
  1. APP_ABI :=后面接的是需要生成的.so平台文件
  2. APP_PLATFORM :=后面接的是使用SDK的最低等级
    Android.mk 源文件如下
# 当前路径
LOCAL_PATH := $(call my-dir)
# CLEAR_VARS 变量由Build System提供。并指向一个指定的GNU Makefile,由它负责清理很多LOCAL_xxx
include $(CLEAR_VARS)
# LOCAL_MODULE模块必须定义,以表示Android.mk中的每一个模块。名字必须唯一且不包含空格
LOCAL_MODULE           := test
# 编译 选项
LOCAL_CFLAGS           := -std=c11 -Weverything -Werror -O0
# 自定义头文件路径
LOCAL_C_INCLUDES       := $(LOCAL_PATH)
# LOCAL_SRC_FILES变量必须包含将要打包如模块的C/C++ 源码
LOCAL_SRC_FILES        := xc_test.c
# 编译成静态库
include $(BUILD_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE           := xcrash
LOCAL_CFLAGS           := -std=c11 -Weverything -Werror -fvisibility=hidden
LOCAL_LDLIBS           := -ldl -llog
LOCAL_STATIC_LIBRARIES := test
LOCAL_C_INCLUDES       := $(LOCAL_PATH) $(LOCAL_PATH)/../../common
LOCAL_SRC_FILES        := xc_jni.c      \
                          xc_common.c   \
                          xc_crash.c    \
                          xc_trace.c    \
                          xc_dl.c       \
                          xc_fallback.c \
                          xc_util.c     \
                          $(wildcard $(LOCAL_PATH)/../../common/*.c)
  # 编译为动态库                        
include $(BUILD_SHARED_LIBRARY)

从上面源码可以获取:

  1. xc_test.c 源码编译为一个静态库libtest.a
  2. 编译生成一个libxcrash.so的动态库。
  3. 在install.sh脚本中也可以看出,拷贝文件的时候是将可执行文件“xcrash_dumper”,重名为“libxcrash_dumper.so”。

编译xcrash_dumper

libxcrash_dumper 文件夹下面也有Application.mk和Android.mk。Application.mk的文件内核和libxcrash 一样,不重复写。
libxcrash_dumper下的Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE           := xcrash_dumper
LOCAL_CFLAGS           := -std=c11 -Weverything -Werror -fvisibility=hidden -fPIE
LOCAL_LDFLAGS          := -pie
LOCAL_LDLIBS           := -ldl -llog
LOCAL_STATIC_LIBRARIES := lzma
LOCAL_C_INCLUDES       := $(LOCAL_PATH) $(LOCAL_PATH)/../../common
LOCAL_SRC_FILES        := $(wildcard $(LOCAL_PATH)/*.c) $(wildcard $(LOCAL_PATH)/../../common/*.c)
# 编译成一个可执行文件
include $(BUILD_EXECUTABLE)
include $(LOCAL_PATH)/lzma/Android.mk

根据源码可知:

  1. xcrash_dumper 最终编译成一个可执行文件。因此在install中xcrash_dumper是重命名为libxcrash_dumper.so。
  2. 同时还有一个Android.mk。在lzma
  3. LZMA,(Lempel-Ziv-Markov chain-Algorithm的缩写)是一个开源的压缩算法。
    lzma文件夹下的Android.mk的文件内容如下
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE            := lzma
LOCAL_CFLAGS            := -std=c11 -Weverything -Werror \
                           -Wno-enum-conversion \
                           -Wno-reserved-id-macro \
                           -Wno-undef \
                           -Wno-missing-prototypes \
                           -Wno-missing-variable-declarations \
                           -Wno-cast-align \
                           -Wno-sign-conversion \
                           -Wno-assign-enum \
                           -Wno-unused-macros \
                           -Wno-padded \
                           -Wno-cast-qual \
                           -Wno-strict-prototypes \
                           -fPIE \
                           -D_7ZIP_ST
LOCAL_C_INCLUDES        := $(LOCAL_PATH)
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
LOCAL_SRC_FILES         := 7zCrc.c      \
                           7zCrcOpt.c   \
                           CpuArch.c    \
                           Bra.c        \
                           Bra86.c      \
                           BraIA64.c    \
                           Delta.c      \
                           Lzma2Dec.c   \
                           LzmaDec.c    \
                           Sha256.c     \
                           Xz.c         \
                           XzCrc64.c    \
                           XzCrc64Opt.c \
                           XzDec.c
include $(BUILD_STATIC_LIBRARY)

通过源码可以确定是编译成为一个静态库liblzma.a

libxcrash

由于xCrah是通过JNI调用C/C++代码,因此需要确定JNI调用的接口,通过查询代码确认libxcrash文件夹内的xc_jni.c是JNI调用的入口,因此从此处开始分析源码。
通过上面的Android.mk可以知,xc_jni.c ; xc_common.c;xc_crash.c;xc_trace.c; xc_dl.c; xc_fallback.c;
xc_util.c ;以及common文件夹内的源文件编译成一个动态库。

  • 下面对源码功能进行简单介绍
xc_jni.c : JNI调用接口
xc_common.c: 日志文件操作
;xc_crash.c: 捕获natvie异常的核心功能
xc_dl.c:通过堆栈地址获取函数名
xc_fallback.c:获取堆栈信息
xc_util.c:常用的处理工具

xc_jni

xc_jni的功能是JNI的调用接口。

加载libxcrash.so

当Java层代码中执行

System.loadLibrary("xcrash");

Native 中的 JNI_OnLoad(JavaVM *vm, void *reserved) 方法会被调用。此时可以注册对应于Java层调用的navtive方法。

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JNIEnv *env;
    jclass  cls;

    (void)reserved;

    if(NULL == vm) return -1;
    
    //register JNI methods
    if(JNI_OK != (*vm)->GetEnv(vm, (void**)&env, XC_JNI_VERSION)) return -1;
    if(NULL == env || NULL == *env) return -1;
    // 查找类  XC_JNI_CLASS_NAME
    if(NULL == (cls = (*env)->FindClass(env, XC_JNI_CLASS_NAME))) return -1;
    // 注册java 层native 接口
    if((*env)->RegisterNatives(env, cls, xc_jni_methods, sizeof(xc_jni_methods) / sizeof(xc_jni_methods[0]))) return -1;
	// 保存信息
    xc_common_set_vm(vm, env, cls);

    return XC_JNI_VERSION;
}
  1. 通过注册接口实现java层native接口转换为C/C++接口。具体是
nativeInit   ----> xc_jni_init
nativeNotifyJavaCrashed ----> xc_jni_notify_java_crashed
nativeTestCrash ----> xc_jni_test_crash
  1. 保存java层信息
// libxcrash/
/**
* JavaVM *vm : 虚拟机在JNI中的表示
* JNIEnv *env 类型是一个指向全部JNI方法的指针
* jclass cls  调用JNI的类的引用
**/
void xc_common_set_vm(JavaVM *vm, JNIEnv *env, jclass cls)
{
	// 在进程信息中保存 
    xc_common_vm = vm;
  // 创建一个新的全局类引用,并保存在全局的进程信息中。
    xc_common_cb_class = (*env)->NewGlobalRef(env, cls);
    // 检查是否有异常
    XC_JNI_CHECK_NULL_AND_PENDING_EXCEPTION(xc_common_cb_class, err);
    return;

 err:
    xc_common_cb_class = NULL;
}

libxcrash.so 初始化

java 层中调用

    private static native int nativeInit(
            int apiLevel,
            String osVersion,
            String abiList,
            String manufacturer,
            String brand,
            String model,
            String buildFingerprint,
            String appId,
            String appVersion,
            String appLibDir,
            String logDir,
            boolean crashEnable,
            boolean crashRethrow,
            int crashLogcatSystemLines,
            int crashLogcatEventsLines,
            int crashLogcatMainLines,
            boolean crashDumpElfHash,
            boolean crashDumpMap,
            boolean crashDumpFds,
            boolean crashDumpAllThreads,
            int crashDumpAllThreadsCountMax,
            String[] crashDumpAllThreadsWhiteList,
            boolean traceEnable,
            boolean traceRethrow,
            int traceLogcatSystemLines,
            int traceLogcatEventsLines,
            int traceLogcatMainLines,
            boolean traceDumpFds);

Native中的xc_jni_init 会被调用

static jint xc_jni_init(JNIEnv       *env,
                        jobject       thiz,
                        jint          api_level,
                        jstring       os_version,
                        jstring       abi_list,
                        jstring       manufacturer,
                        jstring       brand,
                        jstring       model,
                        jstring       build_fingerprint,
                        jstring       app_id,
                        jstring       app_version,
                        jstring       app_lib_dir,
                        jstring       log_dir,
                        jboolean      crash_enable,
                        jboolean      crash_rethrow,
                        jint          crash_logcat_system_lines,
                        jint          crash_logcat_events_lines,
                        jint          crash_logcat_main_lines,
                        jboolean      crash_dump_elf_hash,
                        jboolean      crash_dump_map,
                        jboolean      crash_dump_fds,
                        jboolean      crash_dump_all_threads,
                        jint          crash_dump_all_threads_count_max,
                        jobjectArray  crash_dump_all_threads_whitelist,
                        jboolean      trace_enable,
                        jboolean      trace_rethrow,
                        jint          trace_logcat_system_lines,
                        jint          trace_logcat_events_lines,
                        jint          trace_logcat_main_lines,
                        jboolean      trace_dump_fds)

xc_jni_init 的主要功能有两个:

  1. 保存java层的设置的信息。
  2. 调用核心功能初始化函数xc_crash_init。
  • 保存信息
    通过libxcrash.so的加载和初始化,xcrash的通用信息已经保存好了。这些信息在日志输出的时候可以用于进程信息,应用信息输出到日志文件中。

在xc_common.c中申明了全局变量用于存储通用信息,通用信息分为以下几类。

  • android系统信息(system info)
    android 的系统信息在libxcrash.so初始化的时候通过接口xc_common_init进行保存
变量 含义
int xc_common_api_level api level
char * xc_common_os_version os version
char * xc_common_abi_list 支持的CPU指令集
char *xc_common_manufacturer 硬件产商
char *xc_common_brand 产品品牌
char *xc_common_model 产品名称
char *xc_common_build_fingerprint 设备指纹
char *xc_common_kernel_version 内核版本
char* xc_common_time_zone 时区

备注
在定制化时,可以在此部分新增一些变量,比如说硬件序列号等,方便确定唯一台终端。

  • 应用信息(app info)
变量 含义 备注
char * xc_common_app_id 应用的applId
char * xc_common_app_version 应用版本名称 一般Android 应用的 versionName
char * xc_common_app_lib_dir so库加载路径 用于执行 libxcrash_dump.so
char* xc_common_log_dir 日志输出路径
  • 进程信息 (process info)
变量 含义 备注
pid_t xc_common_process_id 进程ID
char * xc_common_process_name 进程名称
uint64_t xc_common_start_time libxcrash.so初始化时间
JavaVM xc_common_vm java 虚拟机在JNI层的引用
jclass xc_common_cb_class 加载libxcrash.so的java类
int xc_common_fd_null 空设备
  • 进程状态信息 (process statue )
变量 含义
sig_atomic_t xc_common_native_crashed 标志产生native 异常
sig_atomic_t xc_common_java_crashed 标志产生java 异常
  • 捕获Native crash核心功能初始
    捕获native crash 的功能的核心代码为xc_crash.c,其中初始化函数为xc_crash_init。
    xc_crash_init函数的源码如下:
int xc_crash_init(...){
	// 打开设备 /dev/null
	xc_crash_prepared_fd = XCC_UTIL_TEMP_FAILURE_RETRY(open("/dev/null", O_RDWR));
	// 标志是否将异常抛给java 层
    xc_crash_rethrow = rethrow;
    // 申请空间保存,异常日志头部信息
    if(NULL == (xc_crash_emergency = calloc(XC_CRASH_EMERGENCY_BUF_LEN, 1))) return XCC_ERRNO_NOMEM;
    // 创建文件路径
    if(NULL == (xc_crash_dumper_pathname = xc_util_strdupcat(xc_common_app_lib_dir, "/"XCC_UTIL_XCRASH_DUMPER_FILENAME))) return XCC_ERRNO_NOMEM;
    // 根据API创建初始化函数堆栈解析库
    // api >= 16 && api <= 20 使用 libcorkscrew.so
    // api >-21 && api <=23 使用 libunwind.so
    xcc_unwind_init(xc_common_api_level);
    // 初始化线程用于将异常抛出给java层
    xc_crash_init_callback(env);
    // 信息保存
    ... 
    // fork 或者clone 
    #ifndef __i386__
    if(NULL == (xc_crash_child_stack = calloc(XC_CRASH_CHILD_STACK_LEN, 1))) return XCC_ERRNO_NOMEM;
    xc_crash_child_stack = (void *)(((uint8_t *)xc_crash_child_stack) + XC_CRASH_CHILD_STACK_LEN);
#else
    if(0 != pipe2(xc_crash_child_notifier, O_CLOEXEC)) return XCC_ERRNO_SYS;
#endif
  
    // 注册信号处理器
    return xcc_signal_crash_register(xc_crash_signal_handler);
   
}

小结
从初始化函数中需要特别注意的有以下几点:

  1. native 回调Java层的接口。
  2. 如何fork一个进程对父进程进行监控
  3. 如何拦截信号,已经信号如何处理。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!