Android未捕获异常机制

一.Framework层的未捕获异常
二.Framework层未捕获异常避免弹窗方案
三.Native层的未捕获异常机制
四.Native层收集crash原理
五.Native层未捕获异常避免弹窗方案

一.Framework层的未捕获异常:

先说几个结论:
①只要把异常传给了系统,进程和进程组就会被干掉,不管哪个线程出现异常;
②如果异常不传给系统,主线程出现未捕获异常,进程也会死亡,但子线程不会。

从进程启动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/

二.Framework层未捕获异常避免弹窗方案

这里可以有两种方法,一种是我们自己设置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层的未捕获异常

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<uintptr_t>(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原理

前面说了,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

五.Native层未捕获异常避免弹窗方案

根据前面的表述,要么是不把这个信号量交给系统去处理,要么也是像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<uintptr_t>(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之类的。

猜你喜欢

转载自blog.csdn.net/aa642531/article/details/90110618