Android 是怎么捕捉 native 异常的

作者: 谁动了我的代码 | 来源:发表于2023-02-03 21:08 被阅读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 的几种方案,都是采用信号量捕捉的方案来做:

    • 在 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做不同的恢复措施
    • 可以针对业务不断改进和适应

    相关文章

      网友评论

        本文标题:Android 是怎么捕捉 native 异常的

        本文链接:https://www.haomeiwen.com/subject/ujbnhdtx.html