xCrash crash 分析

635 阅读2分钟

前言

整体逻辑与 anr 分析时差不多,也是监听各种信号,然后在信号到来时进行处理。入口函数是 xc_crash_init()

image.png

相较于 anr,crash 时监听的信号多很多,但统一的处理函数都是 xc_crash_signal_handler()

xc_crash_signal_handler

代码虽然很多,但整体上分为三部分

第一部分,fork() 前的准备。主要是通过 prctl() 设置一些属性,当然在方法的最后还会恢复这些属性,主要逻辑如下:

//set dumpable
orig_dumpable = prctl(PR_GET_DUMPABLE);
errno = 0;
// 修改当前调用进程/线程的某些属性
// 第一个参数指定要修改什么。类似于 ioctl() 方法,需要指明要执行的操作
if(0 != prctl(PR_SET_DUMPABLE, 1))
{}

//set traceable (disable the ptrace restrictions introduced by Yama)
//https://www.kernel.org/doc/Documentation/security/Yama.txt
errno = 0;
if(0 != prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY))
{
    // ...
}
else
{
    // ...
}
// 恢复属性的也是通过 prctl() 完成,省略

第二部分:fork 出子进程并在子进程中执行相应的 dump 操作,这一部分是主要是 xc_crash_fork() 方法 第三部分:父进程使用 waitpid() 等待子进程结束,同时会记录一些结束状态。关于状态的记录参考 blog.51cto.com/u_13999641/…

image.png

以上就是关于 crash 记录的大致流程,主要逻辑还是在 xc_crash_fork() 中

xc_crash_fork

主要流程如下:

image.png

最核心的逻辑就是调用了传入 xc_crash_fork 的函数 xc_crash_exec_dumper。如下:

xc_crash_exec_dumper

分片看。

第一段,禁止标准输出和标准错误

image.png

第二段,通过 pipe 将一些信息写入到标准输入中,主要为了方便子进程读取这些信息。这一部分

int pipefd[2];
errno = 0;
if(0 != pipe2(pipefd, O_CLOEXEC)) // 创建 pipe
{

    return 92;
}

int write_len = ... ; // 省略计算
errno = 0;
// fcntl() 描述符操作函数
// 第二个参数指定要执行哪个操作,第三个是传入操作的参数
// 这行代码主要是用来设置了 pipe[1] 的 buffer 大小
if(fcntl(pipefd[1], F_SETPIPE_SZ, write_len) < write_len)
{

}

//write args to pipe
struct iovec iovs[12] = {}; // 省略要写入的值

int iovs_cnt = (0 == xc_crash_spot.dump_all_threads_whitelist_len ? 11 : 12);
errno = 0;
// 通过 writev 写入数据,最终通过 pipefd[0] 可以读取到写入的数据
ssize_t ret = XCC_UTIL_TEMP_FAILURE_RETRY(writev(pipefd[1], iovs, iovs_cnt));
if((ssize_t)write_len != ret)
{

}

// 标准输入重定向到 pipefd[0],因此通过标准输入就可以读取到上面写入的数据
XCC_UTIL_TEMP_FAILURE_RETRY(dup2(pipefd[0], STDIN_FILENO));

syscall(SYS_close, pipefd[0]);
syscall(SYS_close, pipefd[1]);

第三部分,执行真正的导出。fork() 后子进程与父进程基本一致,通过 execl() 相当于清空子进程,然后执行指定 so 的 main() 函数。这里就是执行 libxcrash_dumper.so 的 main() 函数

execl(xc_crash_dumper_pathname, XCC_UTIL_XCRASH_DUMPER_FILENAME, NULL);

main

juejin.cn/post/684490…

整体分析过程需要 elf 文件,寄存器相关的知识,玩不明白。只记录一些收获 :

  1. 使用 ptrace(PTRACE_GETREGS, tid) 可获取线程寄存器
  2. 使用 ptrace(PTRACE_ATTACH, tid) 及 ptrace(PTRACE_DETACH, tid) 可暂停恢复线程
  3. 通过读取 /proc/pid/task 目录可拿到进程的所有线程 id。注意:子进程需要传入父进程的 pid 才可拿到父进程的所有线程