Android 是怎么捕捉 native 异常的

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: Android 是怎么捕捉 native 异常的

在参考了 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 的源码来看,会解决一部分的疑惑。

参考文章:

目录
相关文章
|
编解码 Android开发
Android native层实现MediaCodec编码H264/HEVC
Android平台在上层实现mediacodec的编码,资料泛滥,已经不再是难事,今天给大家介绍下,如何在Android native层实现MediaCodec编码H264/HEVC,网上千篇一律的接口说明,这里不再赘述,本文主要介绍下,一些需要注意的点,权当抛砖引玉,相关设计界面如下:
246 0
|
6月前
|
开发工具 Android开发
android studio build异常
android studio build异常
40 3
|
6月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
5月前
|
Dart Android开发 Windows
Flutter和Native 通信 android端
Flutter和Native 通信 android端
|
7月前
|
Android开发
jack-server导致 Android 编译 出现异常
jack-server导致 Android 编译 出现异常
188 6
|
7月前
|
Android开发
android捕获全局异常,并对异常做出处理
android捕获全局异常,并对异常做出处理
76 4
|
7月前
|
存储 安全 文件存储
Android OTA升级后输入法异常和应用丢失的分析
Android OTA升级后输入法异常和应用丢失的分析
112 1
|
7月前
|
Android开发
android 12 U盘 /mnt/media_rw 下读取文件异常 没有权限
android 12 U盘 /mnt/media_rw 下读取文件异常 没有权限
283 0
|
数据库 Android开发 数据库管理
java.lang.NullPointerException: Attempt to invoke virtual method ‘int android.database.sqlite异常
java.lang.NullPointerException: Attempt to invoke virtual method ‘int android.database.sqlite异常
370 0
|
Android开发
Android > Project with path ‘:audiovisualize‘ could not be found in project ‘:app‘. 异常解决方案
Android > Project with path ‘:audiovisualize‘ could not be found in project ‘:app‘. 异常解决方案
88 0