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的操作文档验证的时候还是有编译问题,所以暂时还没有集成到项目的流程里面。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容