美文网首页
“Signal”背后的bug与解决

“Signal”背后的bug与解决

作者: BlueSocks | 来源:发表于2022-11-30 21:40 被阅读0次

    背景

    熟悉我的老朋友可能都知道,之前为了应对crash与anr,开源过一个“民间偏方”的库Signal,用于解决在发生crash或者anr时进行应用的重启,从而最大程度减少其坏影响。

    在维护的过程中,发生过这样一件趣事,就是有位朋友发现在遇到信号为SIGSEGV时,再调用信号处理函数的时候

    void SigFunc(int sig_num, siginfo *info, void *ptr) {
        // 这里判空并不代表这个对象就是安全的,因为有可能是脏内存
    
        if (currentEnv == nullptr || currentObj == nullptr) {
            return;
        }
        __android_log_print(ANDROID_LOG_INFO, TAG, "%d catch", sig_num);
        __android_log_print(ANDROID_LOG_INFO, TAG, "crash info pid:%d ", info->si_pid);
        jclass main = currentEnv->FindClass("com/example/lib_signal/SignalController");
        jmethodID id = currentEnv->GetMethodID(main, "callNativeException", "(ILjava/lang/String;)V");
        if (!id) {
            __android_log_print(ANDROID_LOG_INFO, TAG, "%d !id!id!id!id!id!id!id", sig_num);
            return;
        }
        __android_log_print(ANDROID_LOG_INFO, TAG, "%d 11111111111111111111", sig_num);
    
        jstring nativeStackTrace  = currentEnv->NewStringUTF(backtraceToLogcat().c_str());
        __android_log_print(ANDROID_LOG_INFO, TAG, "%d 22222222222222222222", sig_num);
        currentEnv->CallVoidMethod(currentObj, id, sig_num, nativeStackTrace);
        __android_log_print(ANDROID_LOG_INFO, TAG, "%d 33333333333333333333", sig_num);
    
        // 释放资源
        currentEnv->DeleteGlobalRef(currentObj);
        currentEnv->DeleteLocalRef(nativeStackTrace);
    }
    
    

    会遇到

    image.png

    从上文打印中看到,SIGSEGV被抛出,之后被我们的信号处理函数抓到了,但是却没有被回调到java层,反而变成了SIGABRT。还有就是SIGSEGV被捕获后,却无法通过jni回调给java层的重启处理。本文将从这个例子出发,从踩坑的过程中去学习更多jni知识。

    出现SIGABRT的原因

    首先呢,currentEnv是一个全局的变量,我们一般jni开发的时候,都习惯于保存一个JNIEnv全局的引用,用于后续的调用处理!但是!这样其实是一个风险的操作,比如我们在sigaction注册一个信号处理函数的时候,那么当信号来的时候,我们的信号处理运行在哪个线程呢?

    image.png

    答案是:不确定。当信号处理时,会根据当前内核的调度,可能会在当前发出信号的线程中进行处理,同时也可能会另外开出一个线程进行处理。而我们的JNIEnv,它其实是一个线程相关的资源,或者说是线程本地资源(TLS),如果我们在其他线程中调用到这个JNIEnv,那么会怎么样呢?比如上面例子中的currentEnv,创建在我们的java层的main线程,此时在信号处理函数中调用currentEnv->FindClass,那么不好意思,这个可不属于当前线程的资源,因此linux内核就会发出一个SIGABRT信号,提示着这个操作将被阻断

    java_vm_ext.cc 
    
    void JavaVMExt::JniAbort(const char* jni_function_name, const char* msg) {
        Thread* self = Thread::Current();
        ScopedObjectAccess soa(self);
        ArtMethod* current_method = self->GetCurrentMethod(nullptr);
    
        std::ostringstream os;
        os << "JNI DETECTED ERROR IN APPLICATION: " << msg;
    
        if (jni_function_name != nullptr) {
            os << "\n    in call to " << jni_function_name;
        }
        // TODO: is this useful given that we're about to dump the calling thread's stack?
        if (current_method != nullptr) {
            os << "\n    from " << current_method->PrettyMethod();
        }
    
        if (check_jni_abort_hook_ != nullptr) {
            check_jni_abort_hook_(check_jni_abort_hook_data_, os.str());
        } else {
            // Ensure that we get a native stack trace for this thread.
            ScopedThreadSuspension sts(self, ThreadState::kNative);
            LOG(FATAL) << os.str();
            UNREACHABLE();
        }
    }
    
    

    JniAbort 调用会在所有的方法调用前进行检测,如果使用到了其他线程的JNIEnv,就会发出SIGABRT信号并打印堆栈信息,用于排查

    java_vm_ext.cc:578] JNI DETECTED ERROR IN APPLICATION: thread Thread[3,tid=22651,Native,Thread*=0xb400007c96340270,peer=0x12c4d1a0,"Thread-3"] using JNIEnv* from thread Thread[1,tid=22160,Runnable,Thread*=0xb400007c9630dbe0,peer=0x73467b00,"main"]
    
    

    那么如果我们真的有场景需要通过在信号处理函数中调用到JNIEnv怎么办,其实也很简单,通过javaVm重新获取一个JNIEnv即可,javaVm保证是虚拟机中唯一的,因此可以放在全局变量中,当我们想要在信号处理函数时调用到jni方法,可重新获取当前线程的环境

    信号处理函数中
    
    if (javaVm->GetEnv((void **) &currentEnv, JNI_VERSION_1_4) != JNI_OK) {
        return ;
    }
    
    

    SIGSEGV被捕获但是调用jni无法进行

    我们的例子是这样的,在java层调用一个jni函数,这个函数通过raise调用向自身发送一个SIGSEGV信号

    raise(SIGSEGV);
    
    

    此时我们的信号处理函数能够捕获到这个事件,但是通过currentEnv->CallVoidMethod却无法调用相应的java层方法了,同时log中出现一个StackOverflowError

    Process: com.example.signal, PID: 24575
    java.lang.StackOverflowError: stack size 8192KB
        at com.example.signal.MainActivity.throwNativeCrash(Native Method)
        at com.example.signal.MainActivity.onCreate$lambda-0(MainActivity.kt:23)
        at com.example.signal.MainActivity.$r8$lambda$__atZomnwlT46HKNaZgatRAAqwU(Unknown Source:0)
        at com.example.signal.MainActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
        at android.view.View.performClick(View.java:8160)
    
    

    那么这个究竟是怎么一回事呢?

    首先我们要明白,我们真的是因为栈内存耗尽了出现StackOverflowError了吗?当然不是!我们只是在jni向自己线程发出了一个SIGSEGV信号罢了,怎么跟栈溢出扯上关系了?我们从art虚拟机开始说起

    在art虚拟机中,出现SIGSEGV时,会默认先回调这个方法

    # fault_handler.cc
    // Signal handler called on SIGSEGV.
    static bool art_fault_handler(int sig, siginfo_t* info, void* context) {
        return fault_manager.HandleFault(sig, info, context);
    }
    
    

    核心是方法

    bool FaultManager::HandleFault(int sig, siginfo_t* info, void* context) {
        if (VLOG_IS_ON(signals)) {
            PrintSignalInfo(VLOG_STREAM(signals) << "Handling fault:" << "\n", info);
        }
    
    #ifdef TEST_NESTED_SIGNAL
        // Simulate a crash in a handler.
      raise(SIGSEGV);
    #endif
        针对生成机器码处理
        if (IsInGeneratedCode(info, context, true)) {
            VLOG(signals) << "in generated code, looking for handler";
            for (const auto& handler : generated_code_handlers_) {
                VLOG(signals) << "invoking Action on handler " << handler;
                if (handler->Action(sig, info, context)) {
                    // We have handled a signal so it's time to return from the
                    // signal handler to the appropriate place.
                    return true;
                }
            }
        }
    
        // We hit a signal we didn't handle.  This might be something for which
        // we can give more information about so call all registered handlers to
        // see if it is.
        其他非机器码处理
        if (HandleFaultByOtherHandlers(sig, info, context)) {
            return true;
        }
    
        // Set a breakpoint in this function to catch unhandled signals.
        只是打印了一些log
        art_sigsegv_fault();
        return false;
    }
    
    

    我们可以留意到,在上面有这么一个判断IsInGeneratedCode,如果是则尝试遍历generated_code_handlers_里面的handler对信号处理,那么IsInGeneratedCode是个啥?其实它是指dex字节码编译成机器码这些代码,art虚拟会在编译成机器码的时候,生成一些虚拟机相关的指令,因此如果SIGSEGV是在这些机器码中生成的,那么就要通过generated_code_handlers_里面的处理器去处理,同时如果是非机器码生成的,则走到HandleFaultByOtherHandlers方法中进行处理

    bool FaultManager::HandleFaultByOtherHandlers(int sig, siginfo_t* info, void* context) {
        if (other_handlers_.empty()) {
            return false;
        }
    
        Thread* self = Thread::Current();
    
        DCHECK(self != nullptr);
        DCHECK(Runtime::Current() != nullptr);
        DCHECK(Runtime::Current()->IsStarted());
        for (const auto& handler : other_handlers_) {
            if (handler->Action(sig, info, context)) {
                return true;
            }
        }
        return false;
    }
    
    

    因此我们特别关注一下generated_code_handlers_,other_handlers_(针对默认处理),它们都是一个集合std::vector<FaultHandler*> 我们看到它的添加元素方法,在FaultManager::AddHandler中

    void FaultManager::AddHandler(FaultHandler* handler, bool generated_code) {
        DCHECK(initialized_);
        if (generated_code) {
            generated_code_handlers_.push_back(handler);
        } else {
            other_handlers_.push_back(handler);
        }
    }
    
    

    这里面添加的handler都是FaultHandler的子类,分别是NullPointerHandler,SuspensionHandler,StackOverflowHandler,JavaStackTraceHandler

    image.png

    虽然JavaStackTraceHandler被加入到了other_handlers_,但是依旧会判断是否处于虚拟机code中

    image.png

    在这里我们明白了SIGSEGV虚拟机的默认处理,一般SIGSEGV都会进入上述handler的判断,如果满足了条件就会先执行(之后才执行到我们的信号处理函数,如果系统栈溢出,那么有可能执行不到自己的信号处理器)。本例子中raise(SIGSEGV)向自己的线程抛出了SIGSEGV,如果信号处理器中没有采用Call系列调用到java层的话,那也不会有问题。

    如果调用到了java层,那么就以栈溢出的形式打印log并重新发一个信号值为SIGKILL的信号杀死当前进程。(这里一直有个疑惑点,目前还没在art源码上看到为什么会这样,如果有知道的大佬可劳烦告知)

    Sending signal. PID: 29066 SIG: 9
    
    

    解决方法也比较简单,当我们异常处理器无法在栈异常情况下,我们可以事先采用sigaltstack分配一块栈空间

    stack_t ss;
    if(NULL == (ss.ss_sp = calloc(1, SIGNAL_CRASH_STACK_SIZE))){
        Handle_Exception();
        break;
    }
    ss.ss_size  = SIGNAL_CRASH_STACK_SIZE;
    ss.ss_flags = 0;
    if(0 != sigaltstack(&ss, NULL)) {
        Handle_Exception();
        break;
    }
    
    

    同时设置flag为SA_ONSTACK即可,让信号处理函数有一个安全的栈空间,得以进行后续调用

    sigc.sa_flags = SA_SIGINFO|SA_ONSTACK;
    
    

    小结

    本次算是一个记录,以一个现象例子,更深入了解jni调用,希望读者有所收获,最后继续贴一下项目地址,如果有更多好点子的话,请多多pr!

    github.com/TestPlanB/S…

    本文转自 [https://juejin.cn/post/7170261352022294541],如有侵权,请联系删除。

    相关文章

      网友评论

          本文标题:“Signal”背后的bug与解决

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