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ò)誤:
- Use after free (dangling pointer dereference)
- Heap buffer overflow
- Stack buffer overflow
- Global buffer overflow
- Use after return
- Use after scope
- Initialization order bugs
- Memory leaks
但這種方式付出的代價(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)目的流程里面。