转自:http://blog.sina.com.cn/s/blog_702c2db50102vc2h.html
Android 虽然已经有好几年了,但是NDK的开放速度却非常缓慢,所以目前网络上针对对Android Native Crash的分析说明还比较少,尤其是非常详细的分析方式更难以查询。因此大部分程序员在遇到难以进行addr2line的crash log时,会一筹莫展。事实上这份log中的其他部分同样提供了非常丰富的信息可供解读,所以在这里总结一下对在这方面的一些经验,在这里以Android samples中的hello-jni为参照做了一定的修改产生的crash来进行分析说明。在深入理解错误日志的分析之后,许多难以复制或者几乎不能重现的bug也能够得到有效的解决。以下所有内容为夜莺原创。
内容主要分为一下几个部分:
- 1.Library Symbols (共享库的符号)
- 2.Analyze Tools (可用到的分析工具)
- 3.CrashLog – Header
- 4.CrashLog
– Backtrace(For most crashes) - 5.CrashLog
– Registers - 6.CrashLog
– Memory - 7.CrashLog
– Stack - 8.Library Base Address (共享库在内存中基地址)
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。
即常用的辅助工具
01 addr2line
(
$(ANDROID_NDK)
\toolchains
\arm-linux-androideabi-4.7
\prebuilt
\windows
\bin)
02
#通过backtrace一栏提供的地址查询对应的符号,可以定位到文件,函数,行数.
03 Usage: addr2line –aCfe libs
$(trace_address)
04
05 ndk-stack (android-ndk-r8d
\ndk-stack)
06
#相当于执行多次addr2line, 可以直接针对一份crash log使用,会输出所有backtrace里地址对应的symbol
07 Usage: ndk-stack –sym
$(lib_directory) –dump
$(crash_log_file)
08
09 objdump (android-ndk-r8d
\toolchains
\arm-linux-androideabi-4.7
\prebuilt
\windows
\bin)
10
#Dump the object file. 通过汇编代码定位错误的原因,大部分复杂的问题可以通过这种方式得到解决。
11 Usage: objdump -S
$(objfile) >
$(output_file)
02
03
04
05
06
07
08
09
10
11
3.Crash Log - Header
信息头,包含当前系统版本有关的信息,如果是做平台级的开发,这将有助于定位当前的系统的开发版本。
1 Time: 2014-11-28 17:40:52
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
2
3
4
5
6
7
8
这部分较为容易阅读。所以不再赘述。
4.CrashLog – Backtrace(For most crashes)
即最常用的看backtrace部分,backtrace的地址可用addr2line或者ndk-stack查找对应的symbol,非常直观,大多数的crash都能够通过这种方式解决。
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
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
从上面这份backtrace可以看到包含一个pc地址和后面的symbol。部分错误可以通过只看这里的symbol发现问题所在。而如果想要更准确的定位,则需要借助ndk工具。
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
2
3
4
然后再来看看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
}
17
18
19
20
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这部分信息展示了出错时的运行状态, 当前中断原因是收到SIGSEGV(通常crash也都是因为收到这个信号,也有少数是因为SIGFPE,即除0操作)。错误码是SEGV_MAPERR,常见的段错误。然后出错地址为ddeeaadd。即第39行的地址0xddeeadd。所以已经可以基本确定和指针b有关。
而代码里面接下来便是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
2
3
4
5
6
7
beab238c在第四行,但是注意在第三行末尾有一串类似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。
这部分源码如下:
现在可以回过去查看d0~d3寄存器的最后一个字节,分别是64,65,61,64。为“dead“。因此当前的r1是增加后后的地址。而此时企图对r0处无效的地址0xddeeaadd写入数据,所以出错。并显示错误地址为0xddeeaadd.
objdump,到这里,再提一提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
:
447
c
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
<</span>Java_com_example_hellojni_HelloJni_stringFromJNI>
return (*env)->NewStringUTF(env, "Hello from JNI !");
16: 6828 ldr r0, [r5, #0]
18: 4907 ldr r1, [pc, #28] ; (38<</span>Java_com_example_hellojni_HelloJni_stringFromJNI+0x38>)
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( JNIEnv * env ,
{
}
不要太在意诸如'Java_com_example_hellojni_HelloJni_stringFromJNI','{','}'之类的符号,它只是提供给我们大致的位置信息,并不是完全等同于C语言中的代码段。
之前有通过backtrace #1看到(Java_com_example_hellojni_HelloJni_stringFromJNI+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
<</span>strlen>
8: 4621 mov r1, r4
a: 4602 mov r2, r0
c: 4802 ldr r0, [pc, #8] ; (18 <</span>func_b+0x18>)
}
e: e8bd 4010 ldmia.w sp!, {r4, lr}
12: f7ff bffe b.w 0 <</span>memcpy>
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 <func_a>:"这样的信息,前面的0xxxxxx就代表函数的在lib中的地址,在这里是"0x46c <func_a>:" ,然后加上0x38(56) 就等于0x4a4,这个和之前有一定的差别,原因是stack上保存的会是函数返回地址,但指向的指令是相同的
。
提出基地址的问题是为了进一步说明stack中的地址和backtrace中地址的不同,以及共享库被加载到内存当中指令的存在形式,但是通过比较也可以发现,在所加载的库非常大的时候(例如100M+)前一种方式得到可以用的地址会相对于后一种方式简单许多。
大多数情况下应该是不需要使用计算基地址的方式。但是也有个别的日志信息给出的backtrace不完整,导致难以解析出具体的问题所在。这个时候就需要使用基地址计算的方式得出可用的addr2line地址。
到最后看来,一般只要有一份类似于错误日志的信息文件,通常可以解决绝大部分的问题。那么如果是运行时,可以通过gdb(如果打开corefile的选项更好),或者kill -9(同样需要打开编译选项才行)。还有就是Android系统通常内自带有debuggerd命令可以使用。详情可以从上网查阅。
最后附上本次测试的源码:http://vdisk.weibo.com/s/yVmhF5M5tTuIi
2014.12.01
codenightingale@gamil.com