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 中最重要的是初始化接口,初始化接口主要功能有:
- 加载so动态库
- 初始化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源码的结构如下
从源码结构中可以分为:
- common文件夹,该文件夹内的源码为通用处理函数和方法,可以被libxcrash文件夹内和libxcrash_dumper的原文件调用。
- libxcrash文件夹,该文件夹内的源码编译成libxcrash.so
- libxcrash文件夹,改文件夹内的源码编译成libxcrash_dumper.so
- 三个脚本文件分为是:build.sh;clean.sh;install.sh
脚本文件
- build.sh脚本文件
build.sh脚本文件非常简单,就是使用ndk编译libxcrash和/libxcrash_dumper
#!/bin/bash
ndk-build -C ./libxcrash/jni
ndk-build -C ./libxcrash_dumper/jni
- 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
- 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
- APP_ABI :=后面接的是需要生成的.so平台文件
- 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)
从上面源码可以获取:
- xc_test.c 源码编译为一个静态库libtest.a
- 编译生成一个libxcrash.so的动态库。
- 在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
根据源码可知:
- xcrash_dumper 最终编译成一个可执行文件。因此在install中xcrash_dumper是重命名为libxcrash_dumper.so。
- 同时还有一个Android.mk。在lzma
- 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;
}
- 通过注册接口实现java层native接口转换为C/C++接口。具体是
nativeInit ----> xc_jni_init
nativeNotifyJavaCrashed ----> xc_jni_notify_java_crashed
nativeTestCrash ----> xc_jni_test_crash
- 保存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 的主要功能有两个:
- 保存java层的设置的信息。
- 调用核心功能初始化函数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);
}
小结
从初始化函数中需要特别注意的有以下几点:
- native 回调Java层的接口。
- 如何fork一个进程对父进程进行监控
- 如何拦截信号,已经信号如何处理。
来源:CSDN
作者:cxmfzu
链接:https://blog.csdn.net/cxmfzu/article/details/103548122