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平台上,这些问题基本上都是在执行Native Code的时候报错,而这些信号一般是进程在执行代码的时候出错之后,或者由Kernel或者自己(比如自己调用abort)或者其他有权限的进程发送给它的,进程接收到这些信号之后,由debuggerd进程输出该进程的tomestone日志.
Native Crash问题的分析难度有难有易,容易的基本上有tombestone日志和对应的symbole文件就可以定位, 分析难度大的,主要是指那些随机踩地址问题,这些问题即使拿到了coredump或者ramdump文件,也都很难分析,因为这一类问题发生的时刻,可能与导致问题的原因,时间上可能相差较远,常见的像堆栈溢出,堆栈溢出的时候可能并不会影响到程序的正常运行,但是后面某个时刻,如果该进程访问到了这片已经被污染的内存就会出问题,从问题现场往往很难分析到底是哪里的代码导致的,对于这一类问题,我们的一个思路就是让问题暴露的更早一点,比如只要有堆栈溢出,就报错,程序退出,这样就比较好找到问题原因了.
简单问题分析
- 中止
中止操作很有趣,因为这是刻意而为。执行中止操作可通过多种不同的方法(调用 abort(3)、调用assert(3))来实现,但所有这些方法都涉及到调用 abort
。一般来说,abort
调用会向调用线程发出 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的时候发现访问的地址为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库检测到可能导致安全漏洞的问题时,就会发生 FORTIFY 失败。很多libc库函数都有做这种检测.
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
如果在可执行文件或者库文件的Android.mk里面 加上-fstack-protector
选项,那么编译器会在那些有栈上面分配内存的函数中插入检测代码,以防止缓冲区溢出,例如你的函数里面定义了一个字符数组,那么这个函数就会加上栈保护代码,防止字符数组越界访问,下面是一个栈溢出的示例:
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等函数,然后在分配内存的时候,在另一个区域再分配一个小内存,记录这一次分配的边界等meta信息,编译器会在生成的可执行文件中添加检查代码以便这个进程在访问内存的时候做检查.
添加AddressSanitizer支持以前,访问内存可能是这样的
*address = ...; // or: ... = *address;
添加AddressSanitizer支持以后,访问内存就会变为
if (IsPoisoned(address)) {
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // or: ... = *address;
它能发现以下几种错误:
- 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
但这种方式付出的代价是进程内存会额外增加,同时也会降低程序的运行速度,下面是Google测试的数据,第二列的编译参数为 clang -O2 ,而第三列的编译参数为 clang -O2 -fsanitize=address -fno-omit-frame-pointer
平均下来,程序的运行速度会降低一半,但是相比分析随机踩地址问题过程中遇到的困难,这种性能的损失在研发阶段是可以接受的,但是在我们的机器上按照Google的操作文档验证的时候还是有编译问题,所以暂时还没有集成到项目的流程里面。