Android 是怎么捕捉 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;
image
全文讲解了Android开发中native的异常捕捉;有关更多的Android技术学习可以参考《Android核心技术类目》这个文档链接。dddd
总结
在Android平台,native crash一直是crash里的大头。native crash具有上下文不全、出错信息模糊、难以捕捉等特点,比java crash更难修复。所以一个合格的异常捕获组件也要能达到以下目的:
- 支持在crash时进行更多扩展操作,如:
- 打印logcat和应用日志
- 上报crash次数
- 对不同的crash做不同的恢复措施
- 可以针对业务不断改进和适应