Android Native/Tombstone Crash Log 詳細分析

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。

 

2.Analyze Tools 

即常用的輔助工具

 

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)

 

3.Crash Log - Header

信息頭,包含當前系統版本有關的信息,如果是做平臺級的開發,這將有助於定位當前的系統的開發版本。

 

Time: 2014-11-28 17:40:52
Build description: xxxx
Build: xxxx
Hardware: xxxx
Revision: 0
Bootloader: unknown
Radio: unknown
Kernel: Linux version 3.4.5 xxxx

這部分較爲容易閱讀。所以不再贅述。

 

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

 

從上面這份backtrace可以看到包含一個pc地址和後面的symbol。部分錯誤可以通過只看這裏的symbol發現問題所在。而如果想要更準確的定位,則需要藉助ndk工具。

 

$addr2line -aCfe out/target/production/xxx/symbols/system/lib/libhello-jni.so 4cf
0x4cf
java_com_example_hellojni_HelloJni_stringFromJNI
/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有關。

而代碼裏面接下來便是memcpy的操作。所以很明顯就是在這裏的memcpy有問題。

再看r0是ddeeaadd,r1是beab238c,r2是4,其實這三個寄存器剛好代表memcpy的操作參數。目的地址爲ddeeaadd,源地址加偏移爲beab238c,長度是4。這裏有提到beab238c爲源地址加偏移,原因的話會在後面解釋。

通常我們需要關注的寄存器主要就是r0到pc,下面的32個寄存器的話通常是數據存取時常用,有時也會有重要信息,但一般情況下不會太關注。如果是對這部分不太瞭解的話,也不用擔心,多看一看就自然明白了,筆者在嘗試卻解讀之前也完全沒有接觸過這方面的內容。

 

6.CrashLog – Memory

日誌當中也提供了出錯時寄存器地址裏面的臨近內存信息,信息量同樣很豐富。之前有提到r1是與源地址有關,所以先看看r1(0xbeab238c)附近的內存情況

 

memory near r1:
   beab236c 4f659a18 51825532 518254a5 df0027ad  
   beab237c 00000000 ddeeaadd 518254d3 64616564  
   beab238c 41414100 41714641 a8616987 40e1d040  
   beab239c 4c11cb40 40e1d040 40a2f614 4bdd2c94  
   beab23ac 00000000 41714608 00000001 417093c4  
   beab23bc 40a5f019 4bdd2c94 518215a3 518254bd

 

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。

這部分源碼如下:

Android <wbr>Native/Tombstone <wbr>Crash <wbr>Log <wbr>詳細分析[原創]
這兩段的大致意思爲從r1地址讀取4個字節放到d0~d3,r1地址增加,然後將d0~d3中的數據存入到r0的地址去,同時r0也增加。

現在可以回過去查看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:    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 <</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','{','}'之類的符號,它只是提供給我們大致的位置信息,並不是完全等同於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

[email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章