Android Stability - Native Crash問題概述

Android Native Crash問題主要是指那些接收到特定signal 之后,由debuggerd進(jìn)程生成tombestone日志的問題,最常見的是下面幾種signal:

  sigaction(SIGABRT, &action, nullptr);
  sigaction(SIGBUS, &action, nullptr);
  sigaction(SIGFPE, &action, nullptr);
  sigaction(SIGILL, &action, nullptr);
  sigaction(SIGSEGV, &action, nullptr);
#if defined(SIGSTKFLT)
  sigaction(SIGSTKFLT, &action, nullptr);
#endif
  sigaction(SIGTRAP, &action, nullptr);

之所以叫它為Native Crash,因?yàn)樵贏ndroid平臺(tái)上,這些問題基本上都是在執(zhí)行Native Code的時(shí)候報(bào)錯(cuò),而這些信號(hào)一般是進(jìn)程在執(zhí)行代碼的時(shí)候出錯(cuò)之后,或者由Kernel或者自己(比如自己調(diào)用abort)或者其他有權(quán)限的進(jìn)程發(fā)送給它的,進(jìn)程接收到這些信號(hào)之后,由debuggerd進(jìn)程輸出該進(jìn)程的tomestone日志.

Native Crash問題的分析難度有難有易,容易的基本上有tombestone日志和對(duì)應(yīng)的symbole文件就可以定位, 分析難度大的,主要是指那些隨機(jī)踩地址問題,這些問題即使拿到了coredump或者ramdump文件,也都很難分析,因?yàn)檫@一類問題發(fā)生的時(shí)刻,可能與導(dǎo)致問題的原因,時(shí)間上可能相差較遠(yuǎn),常見的像堆棧溢出,堆棧溢出的時(shí)候可能并不會(huì)影響到程序的正常運(yùn)行,但是后面某個(gè)時(shí)刻,如果該進(jìn)程訪問到了這片已經(jīng)被污染的內(nèi)存就會(huì)出問題,從問題現(xiàn)場(chǎng)往往很難分析到底是哪里的代碼導(dǎo)致的,對(duì)于這一類問題,我們的一個(gè)思路就是讓問題暴露的更早一點(diǎn),比如只要有堆棧溢出,就報(bào)錯(cuò),程序退出,這樣就比較好找到問題原因了.

簡(jiǎn)單問題分析
  • 中止

中止操作很有趣,因?yàn)檫@是刻意而為。執(zhí)行中止操作可通過多種不同的方法(調(diào)用 abort(3)、調(diào)用assert(3))來實(shí)現(xiàn),但所有這些方法都涉及到調(diào)用 abort。一般來說,abort 調(diào)用會(huì)向調(diào)用線程發(fā)出 SIGABRT 信號(hào),因此為了識(shí)別這種情況,只需要在 debuggerd 輸出中查找以下兩項(xiàng)內(nèi)容:libc.so 中顯示“abort”的幀,以及 SIGABRT 信號(hào)。

pid: 4637, tid: 4637, name: crasher  >>> crasher <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'some_file.c:123: some_function: assertion "false" failed'
    r0  00000000  r1  0000121d  r2  00000006  r3  00000008
    r4  0000121d  r5  0000121d  r6  ffb44a1c  r7  0000010c
    r8  00000000  r9  00000000  r10 00000000  r11 00000000
    ip  ffb44c20  sp  ffb44a08  lr  eace2b0b  pc  eace2b16
backtrace:
    #00 pc 0001cb16  /system/lib/libc.so (abort+57)
    #01 pc 0001cd8f  /system/lib/libc.so (__assert2+22)
    #02 pc 00001531  /system/bin/crasher (do_action+764)
    #03 pc 00002301  /system/bin/crasher (main+68)
    #04 pc 0008a809  /system/lib/libc.so (__libc_init+48)
    #05 pc 00001097  /system/bin/crasher (_start_main+38)
  • 空指針

這是典型的原生代碼崩潰問題,雖然它只是下一類崩潰問題的特殊情況,但值得單獨(dú)說明,因?yàn)檫@類崩潰問題通常無需細(xì)細(xì)思量,基本上一眼就能看出來出錯(cuò)的地方,如以下示例,這一類的問題的關(guān)鍵字是: fault addr 0x0

pid: 25326, tid: 25326, name: crasher  >>> crasher <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
    r0 00000000  r1 00000000  r2 00004c00  r3 00000000
    r4 ab088071  r5 fff92b34  r6 00000002  r7 fff92b40
    r8 00000000  r9 00000000  sl 00000000  fp fff92b2c
    ip ab08cfc4  sp fff92a08  lr ab087a93  pc efb78988  cpsr 600d0030

backtrace:
    #00 pc 00019988  /system/lib/libc.so (strlen+71)
    #01 pc 00001a8f  /system/xbin/crasher (strlen_null+22)
    #02 pc 000017cd  /system/xbin/crasher (do_action+948)
    #03 pc 000020d5  /system/xbin/crasher (main+100)
    #04 pc 000177a1  /system/lib/libc.so (__libc_init+48)
    #05 pc 000010e4  /system/xbin/crasher (_start+96)

盡管strlen函數(shù)在 libc.so 中,但因?yàn)閟trlen僅在指定給它們的指針參數(shù)處進(jìn)行操作,所以可以推斷出在調(diào)用 strlen(3)時(shí)傳遞的是 Null指針.

  • fault addr不為0的空指針

在許多情況下,fault addr都不會(huì)為 0,而是其他一些小數(shù)字。兩位或三位的地址尤其常見,但超過六位地址幾乎可以肯定不是空指針 ,因?yàn)檫@需要 1 MiB 的偏移量,通常不會(huì)定義這么大一個(gè)結(jié)構(gòu)體。

pid: 25405, tid: 25405, name: crasher  >>> crasher <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xc
    r0 0000000c  r1 00000000  r2 00000000  r3 3d5f0000
    r4 00000000  r5 0000000c  r6 00000002  r7 ff8618f0
    r8 00000000  r9 00000000  sl 00000000  fp ff8618dc
    ip edaa6834  sp ff8617a8  lr eda34a1f  pc eda618f6  cpsr 600d0030

backtrace:
    #00 pc 000478f6  /system/lib/libc.so (pthread_mutex_lock+1)
    #01 pc 0001aa1b  /system/lib/libc.so (readdir+10)
    #02 pc 00001b35  /system/xbin/crasher (readdir_null+20)
    #03 pc 00001815  /system/xbin/crasher (do_action+976)
    #04 pc 000021e5  /system/xbin/crasher (main+100)
    #05 pc 000177a1  /system/lib/libc.so (__libc_init+48)
    #06 pc 00001110  /system/xbin/crasher (_start+96)

DIR、readdir和pthread_mutex_lock的定義如下,出錯(cuò)的代碼在pthread_mutex_lock里面,它去訪問mutex_interface的時(shí)候發(fā)現(xiàn)訪問的地址為0xc,所以報(bào)錯(cuò),而mutex_interface是由readdir傳過來的DIR結(jié)構(gòu)體的mutex_,而mutex_ 的偏移量 = sizeof(int) + sizeof(size_t) + sizeof(dirent*) = 0xc,所以這個(gè)問題其實(shí)是crasher在調(diào)用readdir的時(shí)候傳了一個(gè)空指針,這也是一類空指針問題.


struct DIR {
    int fd_;
    size_t available_bytes_;
    dirent* next_;
    pthread_mutex_t mutex_;
    dirent buff_[15];
    long current_pos_;
  };

dirent* readdir(DIR* d) {
  ScopedPthreadMutexLocker locker(&d->mutex_);
  return __readdir_locked(d);
}

int pthread_mutex_lock(pthread_mutex_t* mutex_interface) {
#if !defined(__LP64__)
    if (mutex_interface == NULL) {
        return EINVAL;
    }
#endif

    pthread_mutex_internal_t* mutex = __get_internal_mutex(mutex_interface);

    uint16_t old_state = atomic_load_explicit(&mutex->state, memory_order_relaxed);
    uint16_t mtype = (old_state & MUTEX_TYPE_MASK);
    uint16_t shared = (old_state & MUTEX_SHARED_MASK);
    // Avoid slowing down fast path of normal mutex lock operation.
    if (__predict_true(mtype == MUTEX_TYPE_BITS_NORMAL)) {
      if (__predict_true(__pthread_normal_mutex_trylock(mutex, shared) == 0)) {
        return 0;
      }
    }
    return __pthread_mutex_lock_with_timeout(mutex, false, nullptr);
}
  • FORTIFY失敗

FORTIFY 失敗是中止的一種特殊情況,當(dāng) libc庫檢測(cè)到可能導(dǎo)致安全漏洞的問題時(shí),就會(huì)發(fā)生 FORTIFY 失敗。很多l(xiāng)ibc庫函數(shù)都有做這種檢測(cè).

pid: 25579, tid: 25579, name: crasher  >>> crasher <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'FORTIFY: read: prevented 32-byte write into 10-byte buffer'
    r0 00000000  r1 000063eb  r2 00000006  r3 00000008
    r4 ff96f350  r5 000063eb  r6 000063eb  r7 0000010c
    r8 00000000  r9 00000000  sl 00000000  fp ff96f49c
    ip 00000000  sp ff96f340  lr ee83ece3  pc ee86ef0c  cpsr 000d0010

backtrace:
    #00 pc 00049f0c  /system/lib/libc.so (tgkill+12)
    #01 pc 00019cdf  /system/lib/libc.so (abort+50)
    #02 pc 0001e197  /system/lib/libc.so (__fortify_fatal+30)
    #03 pc 0001baf9  /system/lib/libc.so (__read_chk+48) //read(fd, buf, 32),buf是一個(gè)只有10個(gè)元素的數(shù)組
    #04 pc 0000165b  /system/xbin/crasher (do_action+534)
    #05 pc 000021e5  /system/xbin/crasher (main+100)
    #06 pc 000177a1  /system/lib/libc.so (__libc_init+48)
    #07 pc 00001110  /system/xbin/crasher (_start+96)
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Meizu/meizu_PRO6/PRO6:6.0/MRA58K/1490040912:user/test-keys'
Revision: '0'
ABI: 'arm'
pid: 9150, tid: 9396, name: net-thrd-4  >>> com.android.browser <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'FORTIFY: FD_SET: file descriptor >= FD_SETSIZE'
    r0 00000000  r1 000024b4  r2 00000006  r3 cc0bf978
    r4 cc0bf980  r5 cc0bf930  r6 0000000b  r7 0000010c
    r8 cc0becc4  r9 cc0bed58  sl 0000000a  fp 00000002
    ip 00000006  sp cc0bec10  lr f7329e45  pc f732b6f0  cpsr 40000010

backtrace:
    #00 pc 000426f0  /system/lib/libc.so (tgkill+12)
    #01 pc 00040e41  /system/lib/libc.so (pthread_kill+32)
    #02 pc 0001c80b  /system/lib/libc.so (raise+10)
    #03 pc 000199bd  /system/lib/libc.so (__libc_android_abort+34)
    #04 pc 00017570  /system/lib/libc.so (abort+4)
    #05 pc 0001b41f  /system/lib/libc.so (__libc_fatal+16)
    #06 pc 0001b437  /system/lib/libc.so (__fortify_chk_fail+18)
    #07 pc 00046f9d  /system/lib/libc.so (__FD_SET_chk+24) //檢查傳遞的fd參數(shù)是否大于1024
    #08 pc 0000a6fd  /system/lib/libjavacrypto.so
    #09 pc 0000b45d  /system/lib/libjavacrypto.so
    #10 pc 02b0494f  /system/framework/arm/boot.oat (offset 0x2633000)
復(fù)雜問題處理

上面已經(jīng)說過,Native Crash問題當(dāng)中比較難分析的是隨機(jī)踩地址問題,除了抓coredump和ramdump之外,其實(shí)還有幾種加快問題分析的手段。

  • -fstack-protector

如果在可執(zhí)行文件或者庫文件的Android.mk里面 加上-fstack-protector 選項(xiàng),那么編譯器會(huì)在那些有棧上面分配內(nèi)存的函數(shù)中插入檢測(cè)代碼,以防止緩沖區(qū)溢出,例如你的函數(shù)里面定義了一個(gè)字符數(shù)組,那么這個(gè)函數(shù)就會(huì)加上棧保護(hù)代碼,防止字符數(shù)組越界訪問,下面是一個(gè)棧溢出的示例:

static char* smash_stack_dummy_buf;
__attribute__ ((noinline)) static void smash_stack_dummy_function(volatile int* plen) {
  smash_stack_dummy_buf[*plen] = 0;
}

__attribute__ ((noinline)) static int smash_stack(volatile int* plen) {
    printf("crasher: deliberately corrupting stack...\n");
    char buf[128];
    smash_stack_dummy_buf = buf;
    // This should corrupt stack guards and make process abort.
    smash_stack_dummy_function(plen);
    return 0;
}
smash_stack(128);

********************************************我是分割線********************************************************

pid: 26717, tid: 26717, name: crasher  >>> crasher <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'stack corruption detected'
    r0 00000000  r1 0000685d  r2 00000006  r3 00000008
    r4 ffd516d8  r5 0000685d  r6 0000685d  r7 0000010c
    r8 00000000  r9 00000000  sl 00000000  fp ffd518bc
    ip 00000000  sp ffd516c8  lr ee63ece3  pc ee66ef0c  cpsr 000e0010

backtrace:
    #00 pc 00049f0c  /system/lib/libc.so (tgkill+12)
    #01 pc 00019cdf  /system/lib/libc.so (abort+50)
    #02 pc 0001e07d  /system/lib/libc.so (__libc_fatal+24)
    #03 pc 0004863f  /system/lib/libc.so (__stack_chk_fail+6)
    #04 pc 000013ed  /system/xbin/crasher (smash_stack+76)
    #05 pc 00001591  /system/xbin/crasher (do_action+280)
    #06 pc 00002219  /system/xbin/crasher (main+100)
    #07 pc 000177a1  /system/lib/libc.so (__libc_init+48)
    #08 pc 00001144  /system/xbin/crasher (_start+96)
  • AddressSanitizer

除了棧溢出之外,堆內(nèi)存也是需要格外保護(hù)的,AddressSanitizer的原理簡(jiǎn)單來說就是hook malloc和free等函數(shù),然后在分配內(nèi)存的時(shí)候,在另一個(gè)區(qū)域再分配一個(gè)小內(nèi)存,記錄這一次分配的邊界等meta信息,編譯器會(huì)在生成的可執(zhí)行文件中添加檢查代碼以便這個(gè)進(jìn)程在訪問內(nèi)存的時(shí)候做檢查.

添加AddressSanitizer支持以前,訪問內(nèi)存可能是這樣的

*address = ...;  // or: ... = *address;

添加AddressSanitizer支持以后,訪問內(nèi)存就會(huì)變?yōu)?/p>

if (IsPoisoned(address)) {
  ReportError(address, kAccessSize, kIsWrite);
}
*address = ...;  // or: ... = *address;

它能發(fā)現(xiàn)以下幾種錯(cuò)誤:

但這種方式付出的代價(jià)是進(jìn)程內(nèi)存會(huì)額外增加,同時(shí)也會(huì)降低程序的運(yùn)行速度,下面是Google測(cè)試的數(shù)據(jù),第二列的編譯參數(shù)為 clang -O2 ,而第三列的編譯參數(shù)為 clang -O2 -fsanitize=address -fno-omit-frame-pointer



平均下來,程序的運(yùn)行速度會(huì)降低一半,但是相比分析隨機(jī)踩地址問題過程中遇到的困難,這種性能的損失在研發(fā)階段是可以接受的,但是在我們的機(jī)器上按照Google的操作文檔驗(yàn)證的時(shí)候還是有編譯問題,所以暫時(shí)還沒有集成到項(xiàng)目的流程里面。

?著作權(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)容