一.Framework层的未捕获异常
二.Framework层未捕获异常避免弹窗方案
三.Native层的未捕获异常机制
四.Native层收集crash原理
五.Native层未捕获异常避免弹窗方案
先说几个结论:
①只要把异常传给了系统,进程和进程组就会被干掉,不管哪个线程出现异常;
②如果异常不传给系统,主线程出现未捕获异常,进程也会死亡,但子线程不会。
从进程启动ZygoteInit.main开始后,会调用到RuntimeInit.commonInit这个方法里面有一行代码:
Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
也就是说进程启动的时候系统就会默认给我们设置一个UncaughtExHandler.然后看一下这个类的具体实现:
[–>RuntimeInit.java]
private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
try {
//保证crash处理过程不会重入
if (mCrashing) return;
mCrashing = true;
...
// 打印异常堆栈信息
StringBuilder message = new StringBuilder();
message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
final String processName = ActivityThread.currentProcessName();
if (processName != null) {
message.append("Process: ").append(processName).append(", ");
}
message.append("PID: ").append(Process.myPid());
Clog_e(TAG, message.toString(), e);
// 把异常信息传递给系统服务,也就是交给AMS处理。
ActivityManagerNative.getDefault().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
} catch (Throwable t2) {
...
} finally {
//自杀并且退出。
Process.killProcess(Process.myPid());
System.exit(10);
}
}
}
handleApplicationCrash最终会走到handleApplicationCrashInner,再看下AMS是怎么处理这个异常的:
[–>ActivityManagerService.java]
void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
ApplicationErrorReport.CrashInfo crashInfo) {
//将Crash信息写入到Event log
EventLog.writeEvent(EventLogTags.AM_CRASH,...);
//将错误信息添加到DropBox
addErrorToDropBox(eventType, r, processName, null, null, null, null, null, crashInfo);
//正式进入crash处理流程
crashApplication(r, crashInfo);
}
[–>ActivityManagerService.java]
private void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) {
...
// makeAppCrashingLocked里面会杀掉进程和进程组,移除进程里面的服务,window之类的。
if (r == null || !makeAppCrashingLocked(r, shortMsg, longMsg, stackTrace)) {
Binder.restoreCallingIdentity(origId);
return;
}
Message msg = Message.obtain();
msg.what = SHOW_ERROR_MSG;
HashMap data = new HashMap();
data.put("result", result);
data.put("app", r);
msg.obj = data;
//发送消息SHOW_ERROR_MSG,弹出提示crash的对话框,等待用户选择
mUiHandler.sendMessage(msg);
...
}
到这里就是App Crash或者未捕获异常导致系统弹窗的代码了。这个处理流程还是比较复杂的,后面的处理也还是会设计到是否是系统应用有不同的策略等。详细的可以去看:http://gityuan.com/2016/06/24/app-crash/
这里可以有两种方法,一种是我们自己设置UncaughtExHandler,不把异常传给系统,但是一般SDK第三方库为了避免冲掉这个Handler,都会默认把它传下去。这里说一下另外一种方案:
在系统默认给我们设置UncaughtExHandler里面有这样一行代码:
ActivityManagerNative.getDefault().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
那么我们就可以拿到AMN,Hook它的handleApplicationCrash方法,不把这个异常信息发送给系统服务,这样就不会导致弹窗了。
public static void hookAms() throws Exception {
Class> amnClass = Class.forName("android.app.ActivityManagerNative");
Method getDefaultMethod = amnClass.getMethod("getDefault");
final Object IActivityManager = getDefaultMethod.invoke(null);
Field gDefaultField = amnClass.getDeclaredField("gDefault");
gDefaultField.setAccessible(true);
Object gDefault = gDefaultField.get(null);
Class> singleClass = Class.forName("android.util.Singleton");
Field mInstanceField = singleClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
IActivityManager.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("handleApplicationCrash".equals(method.getName())) {
LoggerUtilsKt.logD("handleApplicationCrash invoke");
return null;
}
return method.invoke(IActivityManager, args);
}
});
// 替换掉AMS
mInstanceField.set(gDefault, proxyInstance);
LoggerUtilsKt.logD("hook finish");
}
一些厂商也会修改这部分的代码,屏蔽掉这种弹窗,从Android P开始,这种弹窗也默认不会出现了。
Native异常处理流程:
异常发生->Kernal发送信号量->当前进程捕获到信号量->将crash信息发送给系统服务->系统服务处理->处理完之后再把信息发给AMS。
跟framework层相似,native层系统也会默认给我们一个信号量的处理机制:
[-> linker/debugger.cpp]
__LIBC_HIDDEN__ void debuggerd_init() {
struct sigaction action;
memset(&action, 0, sizeof(action));
sigemptyset(&action.sa_mask);
// 指定信号接收的函数
action.sa_sigaction = debuggerd_signal_handler;
action.sa_flags = SA_RESTART | SA_SIGINFO;
//使用备用signal栈(如果可用),以便我们能捕获栈溢出
action.sa_flags |= SA_ONSTACK;
sigaction(SIGABRT, &action, nullptr);
sigaction(SIGBUS, &action, nullptr);
sigaction(SIGFPE, &action, nullptr);
sigaction(SIGILL, &action, nullptr);
sigaction(SIGPIPE, &action, nullptr);
sigaction(SIGSEGV, &action, nullptr);
#if defined(SIGSTKFLT)
sigaction(SIGSTKFLT, &action, nullptr);
#endif
sigaction(SIGTRAP, &action, nullptr);
}
当内核发送信号量时,就会进入到这个函数处理:
[-> linker/debugger.cpp]
static void debuggerd_signal_handler(int signal_number, siginfo_t* info, void*) {
if (!have_siginfo(signal_number)) {
info = nullptr; //SA_SIGINFO标识被意外清空,则info未定义
}
//输出一些简要signal信息
log_signal_summary(signal_number, info);
//建立于debuggerd的socket通信连接,这个函数比较关键,就是它把crash信息发送给系统服务debuggerd
send_debuggerd_packet(info);
//重置信号处理函数为SIG_DFL(默认操作)
signal(signal_number, SIG_DFL);
switch (signal_number) {
case SIGABRT:
case SIGFPE:
case SIGPIPE:
#if defined(SIGSTKFLT)
case SIGSTKFLT:
#endif
case SIGTRAP:
tgkill(getpid(), gettid(), signal_number);
break;
default: // SIGILL, SIGBUS, SIGSEGV
break;
}
}
[-> linker/debugger.cpp]
static void send_debuggerd_packet(siginfo_t* info) {
...
//建立与debuggerd的socket通道
int s = socket_abstract_client(DEBUGGER_SOCKET_NAME, SOCK_STREAM | SOCK_CLOEXEC);
...
debugger_msg_t msg;
msg.action = DEBUGGER_ACTION_CRASH;
msg.tid = gettid();
msg.abort_msg_address = reinterpret_cast(g_abort_message);
msg.original_si_code = (info != nullptr) ? info->si_code : 0;
//将DEBUGGER_ACTION_CRASH消息发送给debuggerd服务端
ret = TEMP_FAILURE_RETRY(write(s, &msg, sizeof(msg)));
if (ret == sizeof(msg)) {
char debuggerd_ack;
//阻塞等待debuggerd服务端的回应数据
ret = TEMP_FAILURE_RETRY(read(s, &debuggerd_ack, 1));
int saved_errno = errno;
notify_gdb_of_libraries();
errno = saved_errno;
}
close(s);
}
数据发送给debuggerd服务端后,会经过一系列处理,比较复杂,限于篇幅,这里跳过,详细的可以去看:http://gityuan.com/2016/06/25/android-native-crash/
debuggerd服务端处理完信息之后,接着发信息给AMS,也是通过socket。AMS会通过startObservingNativeCrashes方法启动一个监听NativeCrash的线程,线程里面就是监听debuggerd发的信息:
[-> NativeCrashListener.java]
public void run() {
final byte[] ackSignal = new byte[1];
{
// DEBUGGERD_SOCKET_PATH= "/data/system/ndebugsocket"
File socketFile = new File(DEBUGGERD_SOCKET_PATH);
try {
FileDescriptor serverFd = Os.socket(AF_UNIX, SOCK_STREAM, 0);
// 创建socket服务端
final UnixSocketAddress sockAddr = UnixSocketAddress.createFileSystem(
DEBUGGERD_SOCKET_PATH);
Os.bind(serverFd, sockAddr);
Os.listen(serverFd, 1);
while (true) {
FileDescriptor peerFd = null;
try {
// 等待debuggerd建立连接
peerFd = Os.accept(serverFd, null /* peerAddress */);
//获取debuggerd的socket文件描述符
if (peerFd != null) {
//只有超级用户才被允许通过该socket进行通信
StructUcred credentials =
Os.getsockoptUcred(peerFd, SOL_SOCKET, SO_PEERCRED);
if (credentials.uid == 0) {
// 这里面最终也会调用handleApplicationCrashInner,走到framework那套处理流程,这样弹窗就会出来了。
consumeNativeCrashData(peerFd);
}
}
} catch (Exception e) {
Slog.w(TAG, "Error handling connection", e);
} finally {
//应答debuggerd已经建立连接
if (peerFd != null) {
Os.write(peerFd, ackSignal, 0, 1);//写入应答消息
Os.close(peerFd);//关闭socket
...
}
}
}
} catch (Exception e) {
Slog.e(TAG, "Unable to init native debug socket!", e);
}
}
那两篇文章都写得挺详细的,可以多看看。
前面说了,native层发生未捕获的crash后,内核会发一个信号量给我们,这个信号量还是在我们进程出现的,我们同样可以设置这个信号量的接收函数:
const int handledSignals[] = {
// 这几个信号都是致命的.
SIGSEGV, // 信号11 无效的内存引用
SIGABRT, // 信号 6 来自abort函数的终止信号
SIGFPE, // 信号 8 浮点异常
SIGILL, // 信号 4 非法指令
SIGBUS, // 信号 7 总线错误
SIGALRM // 信号 14 警报器发出的信号
};
const int handledSignalsNum = sizeof(handledSignals) / sizeof(handledSignals[0])};
// 旧的信号处理器,每个信号量可以设置不同的处理器。
struct sigaction old_handlers[handledSignalsNum];
// 当发生Native崩溃并且发生前面几个信号异常时,就会调用mySigaction完成信号处理。这个函数里面的info就包含了错误的堆栈信息等。
void mySigaction(int code, siginfo_t *info, void *reserved) {
LOGD("收到信号了!%d", code);
int index = 0;
switch(code){
case SIGSEGV:
index = 0;
break;
case SIGABRT:
index = 1;
break;
case SIGFPE:
index = 2;
break;
case SIGILL:
index = 3;
break;
case SIGBUS:
index = 4;
break;
case SIGALRM :
index = 5;
break;
}
// 再交给旧的处理器去处理
old_handlers[index].sa_sigaction(code, info, reserved);
}
// 开始之前调用一下这个方法,设置新的信号量处理
void setSigaction() {
struct sigaction handler;
memset(&handler, 0, sizeof(struct sigaction));
handler.sa_sigaction = mySigaction;
handler.sa_flags = SA_RESTART | SA_SIGINFO;
// 关键就是这个sigaction函数,第一个参数表示要处理的信号量,第二个表示处理这个信号量新的句柄,第三个是旧的信号处理句柄。
for (int i = 0;i < handledSignalsNum; ++i) {
int result = sigaction(handledSignals[i],
&handler,
&old_handlers[i]);
if (result == 0) {
LOGD("设置信号量成功");
}
}
}
关于信号量:
参考:https://juejin.im/entry/5962e439f265da6c2810c8aa
根据前面的表述,要么是不把这个信号量交给系统去处理,要么也是像framework层那样去hook一些函数,不把crash信息发送给debuggerd服务端。我们回头再看一下这个函数:
static void send_debuggerd_packet(siginfo_t* info) {
...
//建立与debuggerd的socket通道
int s = socket_abstract_client(DEBUGGER_SOCKET_NAME, SOCK_STREAM | SOCK_CLOEXEC);
...
debugger_msg_t msg;
msg.action = DEBUGGER_ACTION_CRASH;
msg.tid = gettid();
msg.abort_msg_address = reinterpret_cast(g_abort_message);
msg.original_si_code = (info != nullptr) ? info->si_code : 0;
//将DEBUGGER_ACTION_CRASH消息发送给debuggerd服务端
ret = TEMP_FAILURE_RETRY(write(s, &msg, sizeof(msg)));
if (ret == sizeof(msg)) {
char debuggerd_ack;
//阻塞等待debuggerd服务端的回应数据
ret = TEMP_FAILURE_RETRY(read(s, &debuggerd_ack, 1));
int saved_errno = errno;
notify_gdb_of_libraries();
errno = saved_errno;
}
close(s);
}
这个函数很独立,就是它把crash信息发送给了系统服务,那么我们是不是可以去hook这个函数,不让它把信息发出去?试了下,发现是不可行的。。。首先这个函数应该是在libc.so库里,要Hook它要先拿到它的符号,但是就是这里失败了,可能是因为这个函数是隐藏的,包括本地初始化信号量处理这个函数,也是使用了__LIBC_HIDDEN__这个符号修饰。
__LIBC_HIDDEN__ void debuggerd_init() {
...
}
第二个原因是这部分的代码其实有点敏感,如果这个函数send_debuggerd_packet被hook了,那么我们完全可以创造恶意应用,触发这个函数时修改参数里面的信息,比如包名什么的伪装成是其他app,甚至手动触发这个函数,导致系统频繁弹窗。所以目前这块没有太好的方案来防止native出现crash而不弹窗,能做的话就是在代码层面上改进,比如尽量使用c++的try/catch之类的。