Android Native Crash 分析指南

1.Library Symbols (共享库的符号)

ndk提供了一些工具可以供程序员直接获取到出错的文件,函数以及行数。 但是这部分工具都需要没有去符号的共享库(通常是放在out/target/product/xxx/symbols/system/lib)。而out/target/product/xxx/system/lib中的共享库是去掉了符号的,所以直接从设备上抓下来的lib是不能够通过工具来找到对应的符号(而且没有去symbol的库比去掉的空间占用会大许多)。所以如果想要分析一份native crash,那么unstripped lib几乎不可缺少,但是即使是strip过的库也同样会包含少量的symbol。

2.Analyze Tools (即常用的辅助工具)

add2line,objdump,ndkstack等等,具体用法baidu或者直接去源码下去找相对应的工具

3.Crash Log – Header

Note:信息头,包含当前系统版本有关的信息,如果是做平台级的开发,这将有助于定位当前的系统的开发版本。

1 Time: XXXXX

2 Build description: xxxx

3 Build: xxxx

4 Hardware: xxxx

5 Revision: 0

6 Bootloader: unknown

7 Radio: unknown

8 Kernel: Linux version 3.4.5 xxxx

这部分较为容易阅读,所以不再赘述。不过有一点需要确认好就是发生问题的版本号,调试这个问题必须找到对应版本的symbol文件,如果版本对应不上,symbol符号可能差距很大

4.CrashLog – Backtrace(For most crashes)

即最常用的看backtrace部分,backtrace的地址可用addr2line或者ndk-stack查找对应的symbol,非常直观,大多数的crash都能够通过这种方式解决。 (需要注意的是,下面backtrace中的地址是针对当前so文件的绝对地址,不是内存的物理地址

01 backtrace:

02    #00  pc 00026fbc  /system/lib/libc.so

03    #01  pc 000004cf  /data/app-lib/com.example.hellojni-1/libhello-jni.so (Java_com_example_hellojni_HelloJni_stringFromJNI+18)

04    #02  pc 0001e610  /system/lib/libdvm.so (dvmPlatformInvoke+112)

05    #03  pc 0004e015  /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+500)

06    #04  pc 00050421  /system/lib/libdvm.so (dvmResolveNativeMethod(unsigned int const*, JValue*, Method const*, Thread*)+200)

07    #05  pc 000279e0  /system/lib/libdvm.so

08    #06  pc 0002b934  /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+180)

09    #07  pc 0006175f  /system/lib/libdvm.so (dvmInvokeMethod(Object*, Method const*, ArrayObject*, ArrayObject*, ClassObject*, bool)+374)

10    #08  pc 00069785  /system/lib/libdvm.so

11    #09  pc 000279e0  /system/lib/libdvm.so

12    #10  pc 0002b934  /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+180)

13    #11  pc 00061439  /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+272)

14    #12  pc 0004a2ed  /system/lib/libdvm.so

15    #13  pc 0004d501  /system/lib/libandroid_runtime.so

16    #14  pc 0004e259  /system/lib/libandroid_runtime.so (android::AndroidRuntime::start(char const*, char const*)+536)

17    #15  pc 00000db7  /system/bin/app_process

18    #16  pc 00020ea0  /system/lib/libc.so (__libc_init+64)

19    #17  pc 00000ae8  /system/bin/app_process

从上面这份backtrace可以看到包含一个pc地址和后面的symbol。部分错误可以通过只看这里的symbol发现问题所在。而如果想要更准确的定位,则需要借助上文中提到的工具。

1 $addr2line -aCfe out/target/production/xxx/symbols/system/lib/libhello-jni.so 4cf

2 0x4cf

3 java_com_example_hellojni_HelloJni_stringFromJNI

4 /ANDROID_PRODUCT/hello-jni/jni/hello-jni.c:48

然后再来看看hello-jni.c

01

17 #include

18 #include

19

20

26 void func_a(char *p);

27 void func_b(char *p);

28 void func_a(char *p)

29 {

30    const char* A = “AAAAAAAAA”; // len = 9

31    char* a = “dead”;

32    memcpy(p, A, strlen(A));

33    memcpy(p, a, strlen(a));

34    p[strlen(a)] = 0;

35    func_b(p);

36 }

37 void func_b(char *p)

38 {

39    char* b = 0xddeeaadd;

40    memcpy(b, p, strlen(p));

41 }

42

43 jstring

44 Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,

45                                                  jobject thiz )

46 {

47    char buf[10];

48    func_a(buf);

49    return (*env)->NewStringUTF(env, “Hello from JNI !”);

50 }

可以看到现在只能看到出错在func_a().

这里面有个比较特别的地方是为什么backtrace 中只有func_a而没有出现func_b. 这是编译器的处理部分,不过多赘述。所以现在只能从backtrace中确认#1是在func_a,然后#0是在libc中的某个函数死掉。其实symbols/system/lib中也包含有libc.so,可以通过addr2line确认是那个函数。而这里调用到libc的只有memcpy, 所以可以基本确定出错在memcpy,但是有三个memcpy,又怎么确定是哪一个呢?(当然,可以通过直接检查代码发现是在func_b里面)

5.CrashLog – Registers

寄存器信息,可以通过这部分信息基本确定系统为什么会错。

01 pid: 4000, tid: 4000, name: xample.hellojni 

02 signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr ddeeaadd

03    r0 ddeeaadd  r1 beab238c  r2 00000004  r3 beab2390

04    r4 4012b260  r5 40e1b760  r6 00000004  r7 4bdd2ca0

05    r8 beab23a8  r9 4bdd2c98  sl 40e1d050  fp beab23bc

06    ip 80000000  sp beab2380  lr 518254d3  pc 400dffbc  cpsr 80000010

07    d0  4141414141414164  d1  6e6a6f6c6c656865

08    d2  3133393766666661  d3  726f6c6f632f3c64

09    d4  3e2d2d206f646f54  d5  6f633c202020200a

10    d6  656d616e20726f6c  d7  3f8000003f800000

11    d8  0000000000000000  d9  0000000000000000

12    d10 0000000000000000  d11 0000000000000000

13    d12 0000000000000000  d13 0000000000000000

14    d14 0000000000000000  d15 0000000000000000

15    d16 000000000000019e  d17 000000000000019e

16    d18 0000000000000000  d19 000000e600000000

17    d20 e600000000000000  d21 0000000000000000

18    d22 0000000000000000  d23 090a0b0c0d0e0f10

19    d24 0000004d0000003d  d25 000000e600000000

20    d26 000000e7000000b7  d27 0000000000000000

21    d28 0000004d0000003d  d29 0000000000000000

22    d30 0000000100000001  d31 0000000100000001

23    scr 60000090

这部分信息展示了出错时的运行状态, 当前中断原因是收到SIGSEGV(通常crash也都是因为收到这个信号,也有少数是因为SIGFPE,即除0操作)。错误码是SEGV_MAPERR,常见的段错误。然后出错地址为ddeeaadd(即第39行的地址0xddeeadd,所以已经可以基本确定和指针b有关)。

Crash 信号列表:

SIGSEGV

Invalid memory reference.

SIGBUS

Access to an undefined portion of a memory object.

SIGFPE

Arithmetic operation error, like divide by zero.

SIGILL

Illegal instruction, like execute garbage or a privileged instruction

SIGSYS

Bad system call.

SIGXCPU

CPU time limit exceeded.

SIGXFSZ

File size limit exceeded.

而代码里面接下来便是memcpy的操作。所以很明显就是在这里的memcpy有问题。

再看r0是ddeeaadd,r1是beab238c,r2是4,其实这三个寄存器刚好代表memcpy的操作参数。目的地址为ddeeaadd,源地址加偏移为beab238c,长度是4。这里有提到beab238c为源地址加偏移,原因看后面解释。

通常我们需要关注的寄存器主要就是r0到pc,下面的32个寄存器的话通常是数据存取时常用,有时也会有重要信息,但一般情况下不会太关注。 

6.CrashLog – Memory

日志当中也提供了出错时寄存器地址里面的临近内存信息,信息量同样很丰富。之前有提到r1是与源地址有关,所以先看看r1(0xbeab238c)附近的内存情况

1 memory near r1:

2    beab236c 4f659a18 51825532 518254a5 df0027ad 

3    beab237c 00000000 ddeeaadd 518254d3 64616564 

4    beab238c 41414100 41714641 a8616987 40e1d040 

5    beab239c 4c11cb40 40e1d040 40a2f614 4bdd2c94 

6    beab23ac 00000000 41714608 00000001 417093c4 

7    beab23bc 40a5f019 4bdd2c94 518215a3 518254bd

beab238c在第四行,但是注意在第三行末尾有一串类似ASCII的字符(参考:ASCII对照表),64616564,这即是dead,而从这里开始,一段内存为64616564 41414100 41714641即”64,65,61,64, 00,41,41,41, 41″647141。其实不难发现这就是dead’\0’AAAA,其后位于栈上的值没有初始化,会比较随机。

所以func_b中p的起始地址应该是从64616564 的位置开始的,至于为什么r1是beab238c,解读一下汇编代码即可很容易发现。

在Android中使用的binoc实现中,查找源文件为memcpy.s(可通过addr2line 找到文件路径和行数)。看到出错点在memcpy.s +248。

这部分源码如下:

247  vld4.8  {d0[0], d1[0], d2[0], d3[0]},  [r1]! 248  vst4.8  {d0[0], d1[0], d2[0], d3[0]},  [r0]!  (note:关于vld,vst请参考ARM Community

这两段的大致意思为从r1地址读取4个字节放到d0~d3,r1地址增加,然后将d0~d3中的数据存入到r0的地址去,同时r0也增加。 现在可以回过去查看d0~d3寄存器的最后一个字节,分别是64,65,61,64。为“dead“。因此当前的r1是增加后后的地址。而此时企图对r0处无效的地址0xddeeaadd写入数据,所以出错。并显示错误地址为0xddeeaadd.

objdump,可以对共享库(.so)使用或者对目标文件(.o)使用,如果共享库比较大,那还是对被编译文件的目标文件使用比较好。通常来说Android的编译会默认保存目标文件,存放在out/target/product/xxxx/obj目录下面,于是现在找到libhello-jni.o通过objdump来查看它的信息。

jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,jobject thiz )

{

  a:    447c          add    r4, pc

  c:    6824          ldr    r4, [r4, #0]

  e:    6821          ldr    r1, [r4, #0]

  10:    9103          str    r1, [sp, #12]

    char buf[10];

    func_a(buf);

  12:    f7ff fffe    bl    0

    return (*env)->NewStringUTF(env, “Hello from JNI !”);

  16:    6828          ldr    r0, [r5, #0]

  18:    4907          ldr    r1, [pc, #28]   ; (38 )

  1a:    f8d0 229c    ldr.w    r2, [r0, #668]   ; 0x29c

  1e:    4628          mov    r0, r5

  20:    4479          add    r1, pc

  22:    4790          blx    r2

}

不要太在意诸如’Java_com_example_hellojni_HelloJni_stringFromJNI’,'{‘,’}’之类的符号,它只是提供给我们大致的位置信息,并不是完全等同于C语言中的代码段。 之前有通过backtrace #1看到(Java_com_example_hellojni_HelloJni_stringFromJNI+18)这样的信息,将+18(这个就是相对函数入口地址的偏移18)转换成16进制为0x12.那么对应dump 出来的文件位置就是上面的12.指令为bl 0.这是一个常见的跳转指令。从源代码里面也可以看到开始调用func_a().

再看看func_b的代码:

void func_b(char *p)

{

  0:    b510          push    {r4, lr}

  2:    4604          mov    r4, r0

  4:    f7ff fffe    bl    0

  8:    4621          mov    r1, r4

  a:    4602          mov    r2, r0

  c:    4802          ldr    r0, [pc, #8]   ; (18 )

}

  e:    e8bd 4010    ldmia.w    sp!, {r4, lr}

  12:    f7ff bffe    b.w    0

  16:    bf00          nop

  18:    ddeeaadd    .word    0xddeeaadd

先将r0(p指针的值)放入r4,调用strlen,返回值默认放入r0(值为4),再将r4取出放入r1,然后从pc+8的位置拿地址放入r0(可以看到func_b+0x18为0xddeeaadd),再跳转到memcpy。所以r0为ddeeaadd,r1为p指针的值,r4为长度。由此进行了memcpy的调用,然后出错。 通过objdump通常可以更进一步的确定错误产生的情况,对追踪代码逻辑有极大的帮助,所以在很多情况下解决问题可以只通过阅读代码,并不需要不停地加debug打印并尝试去复制它。

7.CrashLog – Stack

当backtrace信息量极少时(没有给全函数调用栈),这是重点。 Stack一栏提供的是线程调用栈的信息。可以从右边的一些symbol大致猜测出错的位置。但由于stack上的内容可能残留未初始化或者未清空的信息,又或者存储有其他的数据,所以有时会造成一定的困惑。因此stack上的symbol虽然大部分是本次调用栈的symbol,但不一定全都是。

stack:

      beab2340  4012ac68 

      beab2344  50572968 

      beab2348  4f659a50 

      beab234c  0000002f 

      beab2350  00000038 

      beab2354  50572960 

      beab2358  beab2390  [stack]

      beab235c  4012ac68 

      beab2360  00000071 

      beab2364  400cb528  /system/lib/libc.so

      beab2368  00000208 

      beab236c  4f659a18 

      beab2370  51825532  /data/app-lib/com.example.hellojni-1/libhello-jni.so

      beab2374  518254a5  /data/app-lib/com.example.hellojni-1/libhello-jni.so (func_a+56)

      beab2378  df0027ad 

      beab237c  00000000 

  #00  beab2380  ddeeaadd 

      beab2384  518254d3  /data/app-lib/com.example.hellojni-1/libhello-jni.so (Java_com_example_hellojni_HelloJni_stringFromJNI+22)

  #01  beab2388  64616564

栈是由下往上(frame#02->#01->#00)。 现在可以大致看到从#01到#00,从Java_com_example_hellojni_HelloJni_stringFromJNI进入func_a。但是这里是不能够通过左边的地址直接addr2line得到目标symbol。它是属于在内存当中的相对地址。接下来就会提到如何去通过相对地址计算可用的addr2line地址。

8.Library Base Address (共享库在内存中基地址)

通过地址计算得出可用的addr2line地址。

addr2line需要一份未去symbol的共享库。当代码没有改变时,每次生成的.so的符号位置应该是相同的。所以如果想要得到有效的符号,必须要使用程序运行时对应的未去符号的.so。

jni在运行时可以看到在java中有load_library的动作,这个动作大致可以看做将一个库文件加载到内存当中。因此这个库在内存当中就存在一个加载的基地址,但是根据内存的情况和相应的算法,基地址每次都可能会不一样。addr2line需要的地址是相对于共享库的一个绝对地址。因此现在只要能够得到共享库在内存中的基地址就能够有办法通过stack上的地址计算出可用的addr2line地址。

在上面的stack和backtrace信息当中有(Java_com_example_hellojni_HelloJni_stringFromJNI+22)和(Java_com_example_hellojni_HelloJni_stringFromJNI+18)这两个symbol的相对地址和绝对地址。

所以基地址的计算应该为对应的地址相减:0x518254d3 – 0x000004cf – 0x4 = 0x51825000.

为了验证基地址有效性,可以尝试计算0x518254a5(func_a+56)的符号:0x518254a5 – 0x51825000 = 0x4a5。 然后使用addr2line查询0x4a5得到hello-jni.c:34。

除此之外还有另一种方法计算可用的地址,同样需要stack里提供的个别的symbol信息: 例0x518254a5(func_a+56),然后之前有提到objdump可以直接将.so作为输入,这时会出来整个lib的汇编信息。然后可以从中找到”0xxxxxxxx :”这样的信息,前面的0xxxxxx就代表函数的在lib中的地址,在这里是”0x46c :” ,然后加上0x38(56) 就等于0x4a4,这个和之前有一定的差别,原因是stack上保存的会是函数返回地址,但指向的指令是相同的。

提出基地址的问题是为了进一步说明stack中的地址和backtrace中地址的不同,以及共享库被加载到内存当中指令的存在形式,但是通过比较也可以发现,在所加载的库非常大的时候(例如100M+)前一种方式得到可以用的地址会相对于后一种方式简单许多。

总结:

1.读懂tomstones相关信息 

2.会使用相关的工具 

3.针对文中的绝对地址,相对地址,偏移地址,原地址要区分清楚 

4.熟悉下arm汇编指令

note:本文中很多信息没有更多解释,大家看的时候可以加上自己的有用信息

    原文作者:Persisting
    原文地址: https://www.jianshu.com/p/dda4bce94a1a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞