在参考了 bugly 的文章和 xcrash 等开源实现方案后,发现这两者很好的展现了 native 异常是如何去捕捉的,接下来,会通过分析 xcrash 源码方案的方式来介绍 native 是如何捕捉异常的。
初始化
在初始化 xcrash 的时候,xc_common_init 预先申请了 2 个 fd,避免因为 fd 耗尽异常申请不到 fd ,导致异常信息无法被记录。如果申请不到 fd,则通过预先申请的 fd 进行异常写入:
//create prepared FD for FD exhausted case xc_common_open_prepared_fd(1); xc_common_open_prepared_fd(0); 复制代码
监听
看了下 Android 捕捉 native 的几种方案,都是采用信号量捕捉的方案来做:
- 在 Unix-like 系统中,所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出,如除零、段地址错误等。
- 异常发生时,CPU 通过异常中断的方式,触发异常处理流程。不同的处理器,有不同的异常中断类型和中断处理方式。
- linux把这些中断处理,统一为信号量,可以注册信号量向量进行处理。
- 信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号
xcrash 的信号注册逻辑在 xcc_signal_crash_register 方法中:
int xcc_signal_crash_register(void (*handler)(int, siginfo_t *, void *)) { // ① stack_t ss; if(NULL == (ss.ss_sp = calloc(1, XCC_SIGNAL_CRASH_STACK_SIZE))) return XCC_ERRNO_NOMEM; ss.ss_size = XCC_SIGNAL_CRASH_STACK_SIZE; ss.ss_flags = 0; if(0 != sigaltstack(&ss, NULL)) return XCC_ERRNO_SYS; // ② struct sigaction act; memset(&act, 0, sizeof(act)); sigfillset(&act.sa_mask); act.sa_sigaction = handler; act.sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK; // ③ size_t i; 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; } 复制代码
①:设置额外栈空间。这块在 bugly 的文章有提到,大致意思就是,如果发生了栈溢出异常,系统会在该栈上调用 SIGSEGV 信号函数,由于栈已溢出,无法执行该函数,然后又报异常,一直反复循环下去。所以,需要开辟一块新的空间,使在发生异常的时候,能正常执行信号函数。
②:声明 sigaction 结构体,并指定信号处理函数 handler,该函数为
xc_crash_signal_handler 方法
③:信号注册。sigaction 为发起信号注册,xcrash 注册了如下信号:
- {.signum = SIGABRT} abort 发出的信号
- {.signum = SIGBUS} 非法内存访问
- {.signum = SIGFPE} 浮点异常,如除 0
- {.signum = SIGILL} 非法指令
- {.signum = SIGSEGV} 无效内存访问
- {.signum = SIGTRAP} 断点或陷阱指令
- {.signum = SIGSYS} 系统调用异常
- {.signum = SIGSTKFLT} 栈溢出
处理
在 native 发生异常时,会回调信号 act.sa_sigaction 指定的函数 ,该函数为 xc_crash_signal_handler 方法:
static void xc_crash_signal_handler(int sig, siginfo_t *si, void *uc) { ... // ① //create and open log file if((xc_crash_log_fd = xc_common_open_crash_log(xc_crash_log_pathname, sizeof(xc_crash_log_pathname), &xc_crash_log_from_placeholder)) < 0) goto end; ... //spawn crash dumper process errno = 0; // ② pid_t dumper_pid = xc_crash_fork(xc_crash_exec_dumper); ... //wait the crash dumper process terminated errno = 0; int status = 0; int wait_r = XCC_UTIL_TEMP_FAILURE_RETRY(waitpid(dumper_pid, &status, __WALL)); end: ... // ③、 if(xc_crash_log_fd >= 0) { //record java stacktrace xc_xcrash_record_java_stacktrace(); ... } ④、 //JNI callback xc_crash_callback(); ⑤、 // 重抛异常 if(0 != xcc_signal_crash_queue(si)) goto exit; ... 复制代码
①:根据文件路径打开日志文件,如果打开失败,则使用预置的 fd
②:fork 子进程来处理 crash dump 操作,父进程 waitpid 一直等待子进程处理结束(后面具体讲)
③:记录 java 堆栈写入日志文件,xcrash 实现的方案与 bugly 不同,bugly 是在 native 层获取线程的名称,然后抛给 java 层,java 获取所有线程的名称与之是否匹配,匹配的则取该线程的堆栈即可;xcrash 是通过 hook native 层的方式,获取当前线程堆栈,这样做,需要考虑兼容性问题,目前代码仅支持 21 <= api <= 30
④:JNI 将结果回调给 java 层
⑤:将信号处理重新抛出
解析
crash dump 解析需要单独拿出来说一下。捕捉到的信号内容 siginfo_t 结构体参数有:
- si_signo :Signal number 信号量
- si_errno :An errno value
- si_code :Signal code 错误码
可以根据 signo 信号量来匹配到是哪个信号发生的错误,然后再根据 code 找到大致错误,该代码在 xCrash 的 xcc_util 代码中:
const char* xcc_util_get_signame(const siginfo_t* si) { switch (si->si_signo) { case SIGABRT: return "SIGABRT"; ... default: return "?"; } } const char* xcc_util_get_sigcodename(const siginfo_t* si) { // Try the signal-specific codes... switch (si->si_signo) { case SIGBUS: switch(si->si_code) { case BUS_ADRALN: return "BUS_ADRALN"; ... } break; ... 复制代码
这个地方有个难点,在于怎么解析出 native 的 backtrace。在 bugly 的文章中有介绍:
通过 dladdr() 可以获得共享库加载到内存的起始地址,和 pc 值相减就可以获得相对偏移地址,并且可以获得共享库的名字。
通过 SP 和 FP 所限定的 stack frame,就可以得到母函数的SP和FP,从而得到母函数的 stack frame(PC,LR,SP,FP会在函数调用的第一时间压栈),以此追溯,即可得到所有函数的调用顺序
实现:
- 在 4.1.1 以上,5.0 以下:使用安卓系统自带的 libcorkscrew.so
- 5.0 以上:安卓系统中没有了 libcorkscrew.so,使用自己编译的 libunwind
void xcc_unwind_init(int api_level) { #if defined(__arm__) || defined(__i386__) if(api_level >= 16 && api_level <= 20) { xcc_unwind_libcorkscrew_init(); } #endif if(api_level >= 21 && api_level <= 23) { xcc_unwind_libunwind_init(); } } 复制代码
这个地方有一个问题,目前的 xcrash 版本无法解析 api 版本大于 23 的 backtrace,兼容性有点问题,需要长期迭代,但目前来看,xCrash 似乎已经不维护了。
backtrace 的解析过程:
int xcd_frames_record_backtrace(xcd_frames_t *self, int log_fd) { ... if(0 != (r = xcc_util_write_str(log_fd, "backtrace:\n"))) return r; TAILQ_FOREACH(frame, &(self->frames), link) { //name name = NULL; if(NULL == frame->map) { name = "<unknown>"; } else if(NULL == frame->map->name || '\0' == frame->map->name[0]) { snprintf(name_buf, sizeof(name_buf), "<anonymous:%"XCC_UTIL_FMT_ADDR">", frame->map->start); name = name_buf; } else { if(0 != frame->map->elf_start_offset) { elf = xcd_map_get_elf(frame->map, self->pid, (void *)self->maps); if(NULL != elf) { name_embedded = xcd_elf_get_so_name(elf); if(NULL != name_embedded && strlen(name_embedded) > 0) { snprintf(name_buf, sizeof(name_buf), "%s!%s", frame->map->name, name_embedded); name = name_buf; } } } if(NULL == name) name = frame->map->name; } //offset if(NULL != frame->map && 0 != frame->map->elf_start_offset) { snprintf(offset_buf, sizeof(offset_buf), " (offset 0x%"PRIxPTR")", frame->map->elf_start_offset); offset = offset_buf; } else { offset = ""; } //func if(NULL != frame->func_name) { if(frame->func_offset > 0) snprintf(func_buf, sizeof(func_buf), " (%s+%zu)", frame->func_name, frame->func_offset); else snprintf(func_buf, sizeof(func_buf), " (%s)", frame->func_name); func = func_buf; } else { func = ""; } if(0 != (r = xcc_util_write_format(log_fd, " #%02zu pc %0"XCC_UTIL_FMT_ADDR" %s%s%s\n", frame->num, frame->rel_pc, name, offset, func))) return r; } if(0 != (r = xcc_util_write_str(log_fd, "\n"))) return r; return 0; 复制代码
总结
由于功力尚且,分析的会不到位,有很多点需要琢磨,全当是图个热闹,如果有疑虑,推荐看 bugly 的文章,然后再结合 xCrash 的源码来看,会解决一部分的疑惑。
参考文章: