Android Stability - Native Crash問題概述

Android Native Crash問題主要是指那些接收到特定signal 之后,由debuggerd進程生成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,因為在Android平臺上,這些問題基本上都是在執(zhí)行Native Code的時候報錯,而這些信號一般是進程在執(zhí)行代碼的時候出錯之后,或者由Kernel或者自己(比如自己調用abort)或者其他有權限的進程發(fā)送給它的,進程接收到這些信號之后,由debuggerd進程輸出該進程的tomestone日志.

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

簡單問題分析
  • 中止

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

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)
  • 空指針

這是典型的原生代碼崩潰問題,雖然它只是下一類崩潰問題的特殊情況,但值得單獨說明,因為這類崩潰問題通常無需細細思量,基本上一眼就能看出來出錯的地方,如以下示例,這一類的問題的關鍵字是: 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函數在 libc.so 中,但因為strlen僅在指定給它們的指針參數處進行操作,所以可以推斷出在調用 strlen(3)時傳遞的是 Null指針.

  • fault addr不為0的空指針

在許多情況下,fault addr都不會為 0,而是其他一些小數字。兩位或三位的地址尤其常見,但超過六位地址幾乎可以肯定不是空指針 ,因為這需要 1 MiB 的偏移量,通常不會定義這么大一個結構體。

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的定義如下,出錯的代碼在pthread_mutex_lock里面,它去訪問mutex_interface的時候發(fā)現訪問的地址為0xc,所以報錯,而mutex_interface是由readdir傳過來的DIR結構體的mutex_,而mutex_ 的偏移量 = sizeof(int) + sizeof(size_t) + sizeof(dirent*) = 0xc,所以這個問題其實是crasher在調用readdir的時候傳了一個空指針,這也是一類空指針問題.


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 失敗是中止的一種特殊情況,當 libc庫檢測到可能導致安全漏洞的問題時,就會發(fā)生 FORTIFY 失敗。很多l(xiāng)ibc庫函數都有做這種檢測.

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是一個只有10個元素的數組
    #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參數是否大于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)
復雜問題處理

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

  • -fstack-protector

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

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

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

添加AddressSanitizer支持以前,訪問內存可能是這樣的

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

添加AddressSanitizer支持以后,訪問內存就會變?yōu)?/p>

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

它能發(fā)現以下幾種錯誤:

但這種方式付出的代價是進程內存會額外增加,同時也會降低程序的運行速度,下面是Google測試的數據,第二列的編譯參數為 clang -O2 ,而第三列的編譯參數為 clang -O2 -fsanitize=address -fno-omit-frame-pointer



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

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容