Android開發中Signal背後的bug與解決

背景

熟悉我的老朋友可能都知道,之前為瞭應對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);
}

會遇到

從上文打印中看到,SIGSEGV被拋出,之後被我們的信號處理函數抓到瞭,但是卻沒有被回調到java層,反而變成瞭SIGABRT。還有就是SIGSEGV被捕獲後,卻無法通過jni回調給java層的重啟處理。本文將從這個例子出發,從踩坑的過程中去學習更多jni知識。

出現SIGABRT的原因

首先呢,currentEnv是一個全局的變量,我們一般jni開發的時候,都習慣於保存一個JNIEnv全局的引用,用於後續的調用處理!但是!這樣其實是一個風險的操作,比如我們在sigaction註冊一個信號處理函數的時候,那麼當信號來的時候,我們的信號處理運行在哪個線程呢?

答案是:不確定。當信號處理時,會根據當前內核的調度,可能會在當前發出信號的線程中進行處理,同時也可能會另外開出一個線程進行處理。而我們的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

雖然JavaStackTraceHandler被加入到瞭other_handlers_,但是依舊會判斷是否處於虛擬機code中

在這裡我們明白瞭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…

以上就是Android開發中Signal背後的bug與解決的詳細內容,更多關於Android Signal bug解決的資料請關註WalkonNet其它相關文章!

推薦閱讀: