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平台上,这些问题基本上都是在执行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;

它能发现以下几种错误:

但这种方式付出的代价是进程内存会额外增加,同时也会降低程序的运行速度,下面是Google测试的数据,第二列的编译参数为 clang -O2 ,而第三列的编译参数为 clang -O2 -fsanitize=address -fno-omit-frame-pointer



平均下来,程序的运行速度会降低一半,但是相比分析随机踩地址问题过程中遇到的困难,这种性能的损失在研发阶段是可以接受的,但是在我们的机器上按照Google的操作文档验证的时候还是有编译问题,所以暂时还没有集成到项目的流程里面。

推荐阅读更多精彩内容