異常處理 - Native 層的崩潰捕獲機(jī)制及實(shí)現(xiàn)

在 Android 平臺(tái),native crash 我們可能關(guān)注得比較少,記得在長(zhǎng)沙做開發(fā)那會(huì),基本不會(huì)用到自己寫的 so 庫(kù),集成第三方功能像地圖也就會(huì)拷貝幾個(gè) so 到目錄下,當(dāng)時(shí)連 so 是什么都不知道。后來漸漸的由于項(xiàng)目的特殊性,不能直接集成 bugly 和 qapm 這些,因此后面就被逼著學(xué)會(huì)了 Native 層的崩潰捕獲。雖然實(shí)現(xiàn)起來相對(duì)要比 java 層更難一些,但也并不是很復(fù)雜,我們可以查一些資料或者借鑒一些第三方的開源庫(kù),總結(jié)起來只需要從以下幾個(gè)方面入手即可:

  • 了解 native 層的崩潰處理機(jī)制
  • 捕捉到 native crash 信號(hào)
  • 處理各種特殊情況
  • 解析 native 層的 crash 堆棧

1. 了解 native 層的崩潰處理機(jī)制

開源庫(kù)有 coffeecatch 、 breakpad 等,普通項(xiàng)目中我們可以直接集成 bugly ,由于 bugly 不開源所以借鑒的意義并不大。breakpad 是 google 開源的比較權(quán)威但是代碼體積量大,coffeecatch 實(shí)現(xiàn)簡(jiǎn)潔但存在兼容性問題。其實(shí)無論是 coffeecatch 還是 bugly 又或是我們自己寫,其內(nèi)部的實(shí)現(xiàn)原理肯定都是一致的, 只要我們了解 native 層的崩潰處理機(jī)制,一切便能迎刃而解。

在 Unix-like 系統(tǒng)中,所有的崩潰都是編程錯(cuò)誤或者硬件錯(cuò)誤相關(guān)的,系統(tǒng)遇到不可恢復(fù)的錯(cuò)誤時(shí)會(huì)觸發(fā)崩潰機(jī)制讓程序退出,如除零、段地址錯(cuò)誤等。異常發(fā)生時(shí),CPU 通過異常中斷的方式,觸發(fā)異常處理流程。不同的處理器,有不同的異常中斷類型和中斷處理方式。linux 把這些中斷處理,統(tǒng)一為信號(hào)量,可以注冊(cè)信號(hào)量向量進(jìn)行處理。信號(hào)機(jī)制是進(jìn)程之間相互傳遞消息的一種方法,信號(hào)全稱為軟中斷信號(hào)。

函數(shù)運(yùn)行在用戶態(tài),當(dāng)遇到系統(tǒng)調(diào)用、中斷或是異常的情況時(shí),程序會(huì)進(jìn)入內(nèi)核態(tài)。信號(hào)涉及到了這兩種狀態(tài)之間的轉(zhuǎn)換。

接收信號(hào)的任務(wù)是由內(nèi)核代理的,當(dāng)內(nèi)核接收到信號(hào)后,會(huì)將其放到對(duì)應(yīng)進(jìn)程的信號(hào)隊(duì)列中,同時(shí)向進(jìn)程發(fā)送一個(gè)中斷,使其陷入內(nèi)核態(tài)。注意,此時(shí)信號(hào)還只是在隊(duì)列中,對(duì)進(jìn)程來說暫時(shí)是不知道有信號(hào)到來的。進(jìn)程陷入內(nèi)核態(tài)后,有兩種場(chǎng)景會(huì)對(duì)信號(hào)進(jìn)行檢測(cè):

  • 進(jìn)程從內(nèi)核態(tài)返回到用戶態(tài)前進(jìn)行信號(hào)檢測(cè)
  • 進(jìn)程在內(nèi)核態(tài)中,從睡眠狀態(tài)被喚醒的時(shí)候進(jìn)行信號(hào)檢測(cè)

當(dāng)發(fā)現(xiàn)有新信號(hào)時(shí),便會(huì)進(jìn)入信號(hào)的處理。信號(hào)處理函數(shù)是運(yùn)行在用戶態(tài)的,調(diào)用處理函數(shù)前,內(nèi)核會(huì)將當(dāng)前內(nèi)核棧的內(nèi)容備份拷貝到用戶棧上,并且修改指令寄存器(eip)將其指向信號(hào)處理函數(shù)。接下來進(jìn)程返回到用戶態(tài)中,執(zhí)行相應(yīng)的信號(hào)處理函數(shù)。信號(hào)處理函數(shù)執(zhí)行完成后,還需要返回內(nèi)核態(tài),檢查是否還有其它信號(hào)未處理。如果所有信號(hào)都處理完成,就會(huì)將內(nèi)核?;謴?fù)(從用戶棧的備份拷貝回來),同時(shí)恢復(fù)指令寄存器(eip)將其指向中斷前的運(yùn)行位置,最后回到用戶態(tài)繼續(xù)執(zhí)行進(jìn)程。至此,一個(gè)完整的信號(hào)處理流程便結(jié)束了,如果同時(shí)有多個(gè)信號(hào)到達(dá),會(huì)不斷的檢測(cè)和處理信號(hào)。

2. 捕捉到 native crash 信號(hào)

了解 native 層的崩潰處理機(jī)制,那么我們的實(shí)現(xiàn)方案便是注冊(cè)信號(hào)處理函數(shù),在 native 層可以用 sigaction():

#include <signal.h> 

// signum:代表信號(hào)編碼,可以是除SIGKILL及SIGSTOP外的任何一個(gè)特定有效的信號(hào),如果為這兩個(gè)信號(hào)定義自己的處理函數(shù),將導(dǎo)致信號(hào)安裝錯(cuò)誤。
// act:指向結(jié)構(gòu)體sigaction的一個(gè)實(shí)例的指針,該實(shí)例指定了對(duì)特定信號(hào)的處理,如果設(shè)置為空,進(jìn)程會(huì)執(zhí)行默認(rèn)處理。
// oldact:和參數(shù)act類似,只不過保存的是原來對(duì)相應(yīng)信號(hào)的處理,也可設(shè)置為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 信號(hào).");
}

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 層的崩潰捕獲復(fù)雜就復(fù)雜在需要處理各種特殊情況,雖然一個(gè)函數(shù)就能監(jiān)聽到崩潰信號(hào)回調(diào),但是需要預(yù)防各種其他異常情況的出現(xiàn),我們一一來看下:

3.1 設(shè)置額外棧空間

SIGSEGV 很有可能是棧溢出引起的,如果在默認(rèn)的棧上運(yùn)行很有可能會(huì)破壞程序運(yùn)行的現(xiàn)場(chǎng),無法獲取到正確的上下文。而且當(dāng)棧滿了(太多次遞歸,棧上太多對(duì)象),系統(tǒng)會(huì)在同一個(gè)已經(jīng)滿了的棧上調(diào)用 SIGSEGV 的信號(hào)處理函數(shù),又再一次引起同樣的信號(hào)。我們應(yīng)該開辟一塊新的空間作為運(yùn)行信號(hào)處理函數(shù)的棧。可以使用 sigaltstack 在任意線程注冊(cè)一個(gè)可選的棧,保留一下在緊急情況下使用的空間。(系統(tǒng)會(huì)在危險(xiǎn)情況下把棧指針指向這個(gè)地方,使得可以在一個(gè)新的棧上運(yùn)行信號(hào)處理函數(shù))

/**
 * 先創(chuàng)建一塊 sigaltstack ,因?yàn)橛锌赡苁怯啥褩R绯霭l(fā)出的信號(hào)
 */
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 處理

某些信號(hào)可能在之前已經(jīng)被安裝過信號(hào)處理函數(shù),而 sigaction 一個(gè)信號(hào)量只能注冊(cè)一個(gè)處理函數(shù),這意味著我們的處理函數(shù)會(huì)覆蓋其他人的處理信號(hào)。保存舊的處理函數(shù),在處理完我們的信號(hào)處理函數(shù)后,在重新運(yùn)行老的處理函數(shù)就能完成兼容。

/* Call the old handler. */
void call_old_signal_handler(const int sig, siginfo_t *const info, void *const sc) {
    // 恢復(fù)默認(rèn)應(yīng)該也行吧
    LOGD("sig -> %d", sig);
    handlerMaps->at(sig)->sa_sigaction(sig, info, sc);
}
3.3 防止死鎖或者死循環(huán)
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) */
    // 這里要考慮用非信號(hào)方式防止死鎖
    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 堆棧

關(guān)于解析 native 層的 crash 堆棧解析,并不是一兩句話能說清楚的,因此我們打算單獨(dú)拿一次課來跟大家講。視頻鏈接地址無法發(fā)出來希望大家能夠諒解,因?yàn)橐徽迟N視頻地址文章就會(huì)被簡(jiǎn)書鎖定。大家感興趣的話,可以去我的 csdn 或者掘金找。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容