美文网首页android-native
异常处理 - Native 层的崩溃捕获机制及实现

异常处理 - Native 层的崩溃捕获机制及实现

作者: 红橙Darren | 来源:发表于2021-02-20 16:28 被阅读0次

    在 Android 平台,native crash 我们可能关注得比较少,记得在长沙做开发那会,基本不会用到自己写的 so 库,集成第三方功能像地图也就会拷贝几个 so 到目录下,当时连 so 是什么都不知道。后来渐渐的由于项目的特殊性,不能直接集成 bugly 和 qapm 这些,因此后面就被逼着学会了 Native 层的崩溃捕获。虽然实现起来相对要比 java 层更难一些,但也并不是很复杂,我们可以查一些资料或者借鉴一些第三方的开源库,总结起来只需要从以下几个方面入手即可:

    • 了解 native 层的崩溃处理机制
    • 捕捉到 native crash 信号
    • 处理各种特殊情况
    • 解析 native 层的 crash 堆栈

    1. 了解 native 层的崩溃处理机制

    开源库有 coffeecatchbreakpad 等,普通项目中我们可以直接集成 bugly ,由于 bugly 不开源所以借鉴的意义并不大。breakpad 是 google 开源的比较权威但是代码体积量大,coffeecatch 实现简洁但存在兼容性问题。其实无论是 coffeecatch 还是 bugly 又或是我们自己写,其内部的实现原理肯定都是一致的, 只要我们了解 native 层的崩溃处理机制,一切便能迎刃而解。

    在 Unix-like 系统中,所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出,如除零、段地址错误等。异常发生时,CPU 通过异常中断的方式,触发异常处理流程。不同的处理器,有不同的异常中断类型和中断处理方式。linux 把这些中断处理,统一为信号量,可以注册信号量向量进行处理。信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号。

    函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换。

    接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。进程陷入内核态后,有两种场景会对信号进行检测:

    • 进程从内核态返回到用户态前进行信号检测
    • 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测

    当发现有新信号时,便会进入信号的处理。信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。接下来进程返回到用户态中,执行相应的信号处理函数。信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,会不断的检测和处理信号。

    2. 捕捉到 native crash 信号

    了解 native 层的崩溃处理机制,那么我们的实现方案便是注册信号处理函数,在 native 层可以用 sigaction():

    #include <signal.h> 
    
    // signum:代表信号编码,可以是除SIGKILL及SIGSTOP外的任何一个特定有效的信号,如果为这两个信号定义自己的处理函数,将导致信号安装错误。
    // act:指向结构体sigaction的一个实例的指针,该实例指定了对特定信号的处理,如果设置为空,进程会执行默认处理。
    // oldact:和参数act类似,只不过保存的是原来对相应信号的处理,也可设置为NULL。
    // int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact));
    
    void signal_pass(int code, siginfo_t *si, void *sc) {
        LOGD("捕捉到了 native crash 信号.");
    }
    
    bool installHandlersLocked() {
        if (handlers_installed)
            return false;
    
        // Fail if unable to store all the old handlers.
        for (int i = 0; i < kNumHandledSignals; ++i) {
            if (sigaction(kExceptionSignals[i], NULL, &old_handlers[i]) == -1) {
                return false;
            } else {
                handlerMaps->insert(
                        std::pair<int, struct sigaction *>(kExceptionSignals[i], &old_handlers[i]));
            }
        }
    
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sigemptyset(&sa.sa_mask);
    
        // Mask all exception signals when we're handling one of them.
        for (int i = 0; i < kNumHandledSignals; ++i)
            sigaddset(&sa.sa_mask, kExceptionSignals[i]);
    
        sa.sa_sigaction = signal_pass;
        sa.sa_flags = SA_ONSTACK | SA_SIGINFO;
    
        for (int i = 0; i < kNumHandledSignals; ++i) {
            if (sigaction(kExceptionSignals[i], &sa, NULL) == -1) {
                // At this point it is impractical to back out changes, and so failure to
                // install a signal is intentionally ignored.
            }
        }
        handlers_installed = true;
        return true;
    }
    

    3. 处理各种特殊情况

    Native 层的崩溃捕获复杂就复杂在需要处理各种特殊情况,虽然一个函数就能监听到崩溃信号回调,但是需要预防各种其他异常情况的出现,我们一一来看下:

    3.1 设置额外栈空间

    SIGSEGV 很有可能是栈溢出引起的,如果在默认的栈上运行很有可能会破坏程序运行的现场,无法获取到正确的上下文。而且当栈满了(太多次递归,栈上太多对象),系统会在同一个已经满了的栈上调用 SIGSEGV 的信号处理函数,又再一次引起同样的信号。我们应该开辟一块新的空间作为运行信号处理函数的栈。可以使用 sigaltstack 在任意线程注册一个可选的栈,保留一下在紧急情况下使用的空间。(系统会在危险情况下把栈指针指向这个地方,使得可以在一个新的栈上运行信号处理函数)

    /**
     * 先创建一块 sigaltstack ,因为有可能是由堆栈溢出发出的信号
     */
    static void installAlternateStackLocked() {
        if (stack_installed)
            return;
    
        memset(&old_stack, 0, sizeof(old_stack));
        memset(&new_stack, 0, sizeof(new_stack));
    
        // SIGSTKSZ may be too small to prevent the signal handlers from overrunning
        // the alternative stack. Ensure that the size of the alternative stack is
        // large enough.
        static const unsigned kSigStackSize = std::max(16384, SIGSTKSZ);
    
        // Only set an alternative stack if there isn't already one, or if the current
        // one is too small.
        if (sigaltstack(NULL, &old_stack) == -1 || !old_stack.ss_sp ||
            old_stack.ss_size < kSigStackSize) {
            new_stack.ss_sp = calloc(1, kSigStackSize);
            new_stack.ss_size = kSigStackSize;
    
            if (sigaltstack(&new_stack, NULL) == -1) {
                free(new_stack.ss_sp);
                return;
            }
            stack_installed = true;
        }
    }
    
    3.2 兼容其他 signal 处理

    某些信号可能在之前已经被安装过信号处理函数,而 sigaction 一个信号量只能注册一个处理函数,这意味着我们的处理函数会覆盖其他人的处理信号。保存旧的处理函数,在处理完我们的信号处理函数后,在重新运行老的处理函数就能完成兼容。

    /* Call the old handler. */
    void call_old_signal_handler(const int sig, siginfo_t *const info, void *const sc) {
        // 恢复默认应该也行吧
        LOGD("sig -> %d", sig);
        handlerMaps->at(sig)->sa_sigaction(sig, info, sc);
    }
    
    3.3 防止死锁或者死循环
    void signal_pass(int code, siginfo_t *si, void *sc) {
        /* Ensure we do not deadlock. Default of ALRM is to die.
        * (signal() and alarm() are signal-safe) */
        // 这里要考虑用非信号方式防止死锁
        signal(code, SIG_DFL);
        signal(SIGALRM, SIG_DFL);
        /* Ensure we do not deadlock. Default of ALRM is to die.
         * (signal() and alarm() are signal-safe) */
        (void) alarm(8);
    
        /* Available context ? */
        notifyCaughtSignal();
    
        call_old_signal_handler(code, si, sc);
    
        LOGD("at the end of signal_pass");
    }
    

    4. 解析 native 层的 crash 堆栈

    关于解析 native 层的 crash 堆栈解析,并不是一两句话能说清楚的,因此我们打算单独拿一次课来跟大家讲。视频链接地址无法发出来希望大家能够谅解,因为一粘贴视频地址文章就会被简书锁定。大家感兴趣的话,可以去我的 csdn 或者掘金找。

    相关文章

      网友评论

        本文标题:异常处理 - Native 层的崩溃捕获机制及实现

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