Android开发经验谈Android开发Android技术知识

Android 是怎么捕捉 native 异常的

2023-02-03  本文已影响0人  谁动了我的代码

初始化

在初始化 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 的几种方案,都是采用信号量捕捉的方案来做:

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 注册了如下信号:

处理

在 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 结构体参数有:

可以根据 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会在函数调用的第一时间压栈),以此追溯,即可得到所有函数的调用顺序

实现:

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更难修复。所以一个合格的异常捕获组件也要能达到以下目的:

上一篇下一篇

猜你喜欢

热点阅读