阅读 547

xCrash 详解与源码分析

一、前言

工欲擅其事,必先利其器。当我们的应用发生错误或者崩溃时,如果有一款趁手的日志捕获工具,那将会得心应手的多。今天要学习的是来自 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 的代码看起来非常简洁,层次也十分的清晰,感叹作者的功力之强。而由于个人水平有限,有些地方分析的可能也不是特别深入到位。有什么错误之处也请帮忙指出改正,感谢。