xCrash 详解与源码分析

作者: 仰简 | 来源:发表于2020-02-28 18:34 被阅读0次

    一、前言

    工欲擅其事,必先利其器。当我们的应用发生错误或者崩溃时,如果有一款趁手的日志捕获工具,那将会得心应手的多。今天要学习的是来自 IQiYi 的 xCrash 日志捕获工具。这款工具不管是从质量上还是功能上,都是上乘之作。

    二、xCrash 叙述

    xCrash 能捕获的异常日志包括了 Java Crash、Native Crash 以及 ANR 日志,而我们在 Android 上所发生的异常,其归结起来无非就是这三种。关于这个库,按官方的解释,其主要的优点如下:

    支持 Android 4.0 - 10(API level 14 - 29)。
    支持 armeabi,armeabi-v7a,arm64-v8a,x86 和 x86_64。
    捕获 java 崩溃,native 崩溃和 ANR。
    获取详细的进程、线程、内存、FD、网络统计信息。
    通过正则表达式设置需要获取哪些线程的信息。
    不需要 root 权限或任何系统权限。

    而站在开发的角度来看,其架构也是十分清晰的。下面是官方所提供的架构图。

    image.png

    三、初始化分析

    1.初始化

    初始化的代码如下,似乎 so easy。

    public class MyCustomApplication extends Application {
    
        @Override
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(base);
            
            xcrash.XCrash.init(this);
        }
    }
    

    这里就不进一步贴代码了,只文字说明一下,初始化主要是获取AppId、AppVersion 等基础信息。当然,除此之外,最重要当然是对 JavaCrash Handler、NativeCrash Handler 以及 AnrCrash Handler 的初始化。

    2. JavaCrash Handler 的初始化

    JavaCrashHandler.jpg

    如上图 JavaCrashHandler 实现了接口 UncaughtExceptionHandler,而它的初始化也简单。

    Thread.setDefaultUncaughtExceptionHandler(this);
    

    这样也算利用虚拟机所提供的接口,开始监控 Java Crash 了。另外比较主要的便是其实现的方法uncaughtException,后面再来说。

    3. AnrHandler 的初始化

    AnrHandler 的初始化除了一些参数的设定,然后就是监听 /data/anr 目录的变化。

    fileObserver = new FileObserver("/data/anr/", CLOSE_WRITE) {
                public void onEvent(int event, String path) {
                    try {
                        if (path != null) {
                            String filepath = "/data/anr/" + path;
                            if (filepath.contains("trace")) {
                                handleAnr(filepath);
                            }
                        }
                    } catch (Exception e) {
                        XCrash.getLogger().e(Util.TAG, "AnrHandler fileObserver onEvent failed", e);
                    }
                }
            };
    
            try {
                fileObserver.startWatching();
            } catch (Exception e) {
                fileObserver = null;
                XCrash.getLogger().e(Util.TAG, "AnrHandler fileObserver startWatching failed", e);
            }
    

    当然,我们都知道,在高版本的 Android 系统中,应用已经访问不到 /data/anr 了。xCrash 是不是有提供了其他的实现方案呢?实际上它上捕获了 SIGQUIT 信号,这个是 Android App 发生 ANR 时由 ActivityMangerService 向 App 发送的信号。具体的,在后面再来分析。

    4.NativeHandler 的初始化

    NativeHandler 的初始化要相对复杂一些了,其分为 Java 层和 Native 层。

    4.1 Java 层

    Java 层相对简单,主要是加载 libxcrash.so ,以及进一步调 nativeInit() 进行 native 层的初始化。

    System.loadLibrary("xcrash");
    

    4.2 Native 层

    nativeInit() 所映射的 jni 实现是 xc_jni_init()。在 xc_jni_init 又分了 3 个小步骤来进行初始化。

    xc_common_init

    这里面初始化了一些公共参数,如 os-kernel-version、app_version、appid、log 目录等。其中最重要的是初始化了两个文件 fd ,以应对文件 fd 被耗尽的情况。

        //create prepared FD for FD exhausted case
        xc_common_open_prepared_fd(1);
        xc_common_open_prepared_fd(0);
    

    这两个 fd 分别给了 xc_common_crash_prepared_fd 和 xc_common_trace_prepared_fd。但是这里要注意,它们目前打开的都是 "/dev/null"。

    xc_crash_init
    xcc_unwind_init 初始化 unwinder。

    api_level >= 16 && api_level <= 20 则加载 libcorkscrew.so
    api_level >= 21 && api_level <= 23 则加载 libunwind.so
    

    xc_crash_init_callback 初始化 jni call back。这里主要是初始化了一个 native 的线程,然后通过 eventfd 阻塞等待 native 发生 crash 时向上层 java 发出通知。

    接下来是比较重要的信号注册,通过xcc_signal_crash_register 进行。

    int xcc_signal_crash_register(void (*handler)(int, siginfo_t *, void *))
    {
        stack_t ss;
        .......
        if(0 != sigaltstack(&ss, NULL)) return XCC_ERRNO_SYS;
        ......
        for(i = 0; i < sizeof(xcc_signal_crash_info) / sizeof(xcc_signal_crash_info[0]); i++)
            if(0 != sigaction(xcc_signal_crash_info[i].signum, &act, &(xcc_signal_crash_info[i].oldact)))
                return XCC_ERRNO_SYS;
    
        return 0;
    }
    

    这里看关键的几行,其中 sigalstack 是用于替换信号处理函数栈,有的说法是设置紧急函数栈。其原因是一般情况下,信号处理函数被调用时,内核会在进程的栈上为其创建一个栈帧。但是这里就会有一个问题,如果栈的增长到达了栈的资源限制值(RLIMIT_STACK,使用 ulimit 命令可以查看,一般为 8M),或是栈已经长得太大(没有 RLIMIT_STACK 的限制),以致到达了映射内存(mapped memory)边界,那么此时信号处理函数就没法得到栈帧的分配。
    然后就是通过 sigaction() 进行信号的安装,这里只关注一下它安装哪一些信号。

         {.signum = SIGABRT},abort发出的信号
        {.signum = SIGBUS},非法内存访问
        {.signum = SIGFPE},浮点异常
        {.signum = SIGILL},非法指令
        {.signum = SIGSEGV},无效内存访问
        {.signum = SIGTRAP},断点或陷阱指令
        {.signum = SIGSYS},系统调用异常
        {.signum = SIGSTKFLT}栈溢出
    

    信号的处理函数在 xc_crash_signal_handler。这个在后面再来分析。还有,这里有也准备了一个文件 fd , xc_crash_prepared_fd , 暂时还不清楚与前面 2 个的区别与关系。

    xc_trace_init
    trace 只是针对 Android 5.0 以上,因为其主要是用来获取 ANR 的 trace。xc_trace_init_callback() 只是获取 Java 的 methodId,进一步的主要操作在 xcc_signal_trace_register()。

    int xcc_signal_trace_register(void (*handler)(int, siginfo_t *, void *))
    {
        ......
        //un-block the SIGQUIT mask for current thread, hope this is the main thread
        sigemptyset(&set);
        sigaddset(&set, SIGQUIT);
        if(0 != (r = pthread_sigmask(SIG_UNBLOCK, &set, &xcc_signal_trace_oldset))) return r;
        //register new signal handler for SIGQUIT
        ......
        if(0 != sigaction(SIGQUIT, &act, &xcc_signal_trace_oldact))
        {
            pthread_sigmask(SIG_SETMASK, &xcc_signal_trace_oldset, NULL);
            return XCC_ERRNO_SYS;
        }
        ......
    }
    

    用来处理 SIG_QUIT 的响应函数是 xc_trace_handler() ,这个也是后面再来分析。函数的最后还会启动一个线程,并在线程响应函数xc_trace_dumper中等待 ANR 的发生。这里的等待机制同样是用的 eventfd。

    5.初始化小结

    1. 初始化 JavaCrashHandler,其实现机制是通过 Thread.setDefaultUncaughtExceptionHandler() 注册一个自己的 UncaughtExceptionHandler。

    2. 初始化 AnrHandler,其实现机制是监听 "/data/anr" 文件夹的变化。同时对于 5.0 以上的版本,通过监听 SIGQUIT 来实现。

    3. 初始化 NativeHandler,预留 FD、安装一系列 signal、初始化用于 unwind 的
      libcorkscrew.so 和 libunwind.so ,以及获取相关的函数。

    四、异常处理分析

    1.Java 异常处理

    Java 的异常处理机制比较简单,只要 uncaughtException() 方法中等待异常的回调,然后收集相应的信息即可。这些都比较简单,这里就不详细分析了,感兴趣的可以自己去看。另外,其实现了一个 Util 类用来读取系统的文件,里面有很多值的学习的东西,如获取 meminfo 、获取文件所占用的 fds 等。

    2.ANR 异常处理

    2.1 Java 层的处理

    Java 层的处理在 AnrHandler#handleAnr() 方法中,其也比较简单,就是解析 data/anr/trace.txt 文件,看看有没有自己进程的信息。感兴趣的也可以自己去分析。

    2.2 Native 层的处理

    关于Native 层的 anr 处理,官方有给了具体的实现架构图。那么,对照图,我们来具体看看它是如何实现的。

    image.png

    在 Native 初始化时,我们知道其监听了 SIGQUIT 信号来处理 ANR 的发生,并在 xc_trace_handler() 方法中来进行处理。

    XCC_UTIL_TEMP_FAILURE_RETRY(write(xc_trace_notifier, &data, sizeof(data)));
    

    其主要的实现很简单,就是通过 eventfd 发送一个通知,那这个通知的响应函数是 xc_trace_dumper(),下面来看看它的具体实现。

    前面 2 步打开日志文件 xc_common_open_trace_log() 和 写入头信息 xc_trace_write_header() 感兴趣的可以自己分析。我们重点是要关注其怎么 dump art 的 trace。

    xc_trace_load_symbols 加载符号表
    xc_dl_create() 和 xc_dl_sym() 是里面比较重要的两个函数实现。xc_dl_create 是寻找到 so 被 mmap 所加载的虚拟地址,xc_dl_sym 是计算 so 中相应符号(函数)的虚拟地址。
    其主要是从 libc++.so 中查找符号 _ZNSt3__14cerrE,对的,就是 cerr ;从 libart.so 中查找符号 _ZN3art7Runtime9instance_E 以及 _ZN3art7Runtime14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE 在进程虚拟空间中的地址。针对 L 还需要 _ZN3art3Dbg9SuspendVMEv 和 _ZN3art3Dbg8ResumeVMEv。

    xc_dl_create() 的具体实现在 xc_dl_find_map_start() 获取 so 的基地址、xc_dl_file_open() 通过 mmap 加载 so、xc_dl_parse_elf() 解析 so。这里的解析 so ,其实就是解析 elf 文件,这个比较复杂,需要对 elf 文件格式熟悉。这里就不深分析了。

    xc_trace_libart_runtime_dump 开始 dump

    相关代码如下:

            if(xc_trace_is_lollipop)
                xc_trace_libart_dbg_suspend();
            xc_trace_libart_runtime_dump(*xc_trace_libart_runtime_instance, xc_trace_libcpp_cerr);
            if(xc_trace_is_lollipop)
                xc_trace_libart_dbg_resume();
    

    xc_trace_libart_runtime_dump 就是_ZN3art7Runtime14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE。也就是调用 dump 将对 SIGQUIT 的处理输出到 cerr 中。这里有一个细节,就是在 dump 节,其通过 dup2() 函数将标准的错误输出重定向到了自己的 fd 中。就在这段代码的上面,如下。

            if(dup2(fd, STDERR_FILENO) < 0)
            {
                if(0 != xcc_util_write_str(fd, "Failed to duplicate FD.\n")) goto end;
                goto skip;
            }
    

    接下来就是其他日志的处理了,感兴趣的也可以看一下,比如 logcat 日志的获取、文件 fd、网络日志等。至此,就完成了对 trace 的抓取了。

    3.Native 异常处理

    关于 Native 异常处理,官方给的架构图如下,流程上是很清晰的。


    image.png

    在初始化的时候我们分析到,当发生 native 崩溃时,会在信号处理函数 xc_crash_signal_handler() 进行处理。那么就从这个函数开始分析吧。

    static void xc_crash_signal_handler(int sig, siginfo_t *si, void *uc)
    {
      ......
      pid_t dumper_pid = xc_crash_fork(xc_crash_exec_dumper);
      ......
      int wait_r = XCC_UTIL_TEMP_FAILURE_RETRY(waitpid(dumper_pid, &status, __WALL));
    }
    

    这个函数除了做一些打开文件 fd 等基本的操作之外,其最主要做的事就是通过 xc_crash_fork() 创建一个子进程并等待子进程返回。

    创建的子进程的响应函数是 xc_crash_exec_dumper()。这个函数首先通过 pipe 将一系列的参数,比如进程 pid ,崩溃线程 tid 等,写入到标准的输入当中,其目的是为了子进程从标准的输入当中去读取参数。然后通过 execl() 进入到真正的 dumper 程序。

    static int xc_crash_exec_dumper(void *arg)
    {
      ......
      execl(xc_crash_dumper_pathname, XCC_UTIL_XCRASH_DUMPER_FILENAME, NULL);
    }
    

    这个其实就是通过 execl() 来运行 libxcrash_dumper.so ,当然,它不会再创建新的进程。而 libxcrash_dumper.so 的入口在 xcd_core.c 中的 main() 。可能很多人第一次在 Android 中见到我们熟悉的 C 语言中的 main() 函数吧。

    下面我把 main() 函数都贴出来,整个实现言简意赅,基本反应了上面 dump 架构图的核心逻辑。

    int main(int argc, char** argv)
    {
        (void)argc;
        (void)argv;
        
        //don't leave a zombie process
        alarm(30);
    
        //read args from stdin
        if(0 != xcd_core_read_args()) exit(1);
    
        //open log file
        if(0 > (xcd_core_log_fd = XCC_UTIL_TEMP_FAILURE_RETRY(open(xcd_core_log_pathname, O_WRONLY | O_CLOEXEC)))) exit(2);
    
        //register signal handler for catching self-crashing
        xcc_unwind_init(xcd_core_spot.api_level);
        xcc_signal_crash_register(xcd_core_signal_handler);
    
        //create process object
        if(0 != xcd_process_create(&xcd_core_proc,
                                   xcd_core_spot.crash_pid,
                                   xcd_core_spot.crash_tid,
                                   &(xcd_core_spot.siginfo),
                                   &(xcd_core_spot.ucontext))) exit(3);
    
        //suspend all threads in the process
        xcd_process_suspend_threads(xcd_core_proc);
    
        //load process info
        if(0 != xcd_process_load_info(xcd_core_proc)) exit(4);
    
        //record system info
        if(0 != xcd_sys_record(xcd_core_log_fd,
                               xcd_core_spot.time_zone,
                               xcd_core_spot.start_time,
                               xcd_core_spot.crash_time,
                               xcd_core_app_id,
                               xcd_core_app_version,
                               xcd_core_spot.api_level,
                               xcd_core_os_version,
                               xcd_core_kernel_version,
                               xcd_core_abi_list,
                               xcd_core_manufacturer,
                               xcd_core_brand,
                               xcd_core_model,
                               xcd_core_build_fingerprint)) exit(5);
    
        //record process info
        if(0 != xcd_process_record(xcd_core_proc,
                                   xcd_core_log_fd,
                                   xcd_core_spot.logcat_system_lines,
                                   xcd_core_spot.logcat_events_lines,
                                   xcd_core_spot.logcat_main_lines,
                                   xcd_core_spot.dump_elf_hash,
                                   xcd_core_spot.dump_map,
                                   xcd_core_spot.dump_fds,
                                   xcd_core_spot.dump_network_info,
                                   xcd_core_spot.dump_all_threads,
                                   xcd_core_spot.dump_all_threads_count_max,
                                   xcd_core_dump_all_threads_whitelist,
                                   xcd_core_spot.api_level)) exit(6);
    
        //resume all threads in the process
        xcd_process_resume_threads(xcd_core_proc);
    
    #if XCD_CORE_DEBUG
        XCD_LOG_DEBUG("CORE: done");
    #endif
        return 0;
    }
    

    里面的每一个过程就不再进行分析了,这里只说最重要的一点,其最核心的获取线程的 regs、backtrace 等信息是通过 ptrace 技术来获取的。这里面关于 ptrace,关于 elf 都相对比较复杂,因此不在这里献丑了。

    五、总结

    xCrash 的代码看起来非常简洁,层次也十分的清晰,感叹作者的功力之强。而由于个人水平有限,有些地方分析的可能也不是特别深入到位。有什么错误之处也请帮忙指出改正,感谢。

    相关文章

      网友评论

        本文标题:xCrash 详解与源码分析

        本文链接:https://www.haomeiwen.com/subject/fofqchtx.html