Android Native crash日志分析


  在Android應用crash的類型中,native類型crash應該是比較難的一種了,因為大家接觸的少,然后相對也要多轉幾道工序,所有大部分對這個都比較生疏。雖然相關文章也有很多了,但是我在剛開始學的過程中還是遇到一些問題,下面一一記錄,以便將來翻閱。

  分析native crash 日志需要幾個東西:

  1. addr2line,objdump,ndk-stack等幾個工具
  2. 帶symbols的so文件
  3. log  

  log

  native crash的日志都是從一行星號(*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***)開始,這行星號也是ndk-stack工具用來查找native crash的標志。一個native crash日志例子:

 1 04-16 11:18:00.323 26512 26512 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
 2 04-16 11:18:00.324 26512 26512 F DEBUG   : Build fingerprint: 'nubia/NX531J/NX531J:7.1.1/NMF26F/nubia04130311:user/release-keys'
 3 04-16 11:18:00.324 26512 26512 F DEBUG   : Revision: '0'
 4 04-16 11:18:00.324 26512 26512 F DEBUG   : ABI: 'arm'
 5 04-16 11:18:00.324 26512 26512 F DEBUG   : pid: 26452, tid: 26491, name: Thread-4  >>> com.willhua.opencvstudy <<<
 6 04-16 11:18:00.324 26512 26512 F DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xc8080000
 7 04-16 11:18:00.324 26512 26512 F DEBUG   :     r0 c807a400  r1 c7e80000  r2 00000069  r3 00000069
 8 04-16 11:18:00.324 26512 26512 F DEBUG   :     r4 caa4ca9c  r5 007e9000  r6 00000964  r7 c69838f8
 9 04-16 11:18:00.324 26512 26512 F DEBUG   :     r8 00005c00  r9 00017002  sl 001fa400  fp cac6ec00
10 04-16 11:18:00.324 26512 26512 F DEBUG   :     ip e71aac64  sp c69838e8  lr caa8e949  pc caa8e97c  cpsr 800f0030
11 04-16 11:18:00.326 26512 26512 F DEBUG   : 
12 04-16 11:18:00.326 26512 26512 F DEBUG   : backtrace:
13 04-16 11:18:00.326 26512 26512 F DEBUG   :     #00 pc 0004097c  /data/app/com.willhua.opencvstudy-1/lib/arm/libOpenCV.so (_Z14darkGrayThreadPv+179)
14 04-16 11:18:00.326 26512 26512 F DEBUG   :     #01 pc 000475d3  /system/lib/libc.so (_ZL15__pthread_startPv+22)
15 04-16 11:18:00.326 26512 26512 F DEBUG   :     #02 pc 00019d3d  /system/lib/libc.so (__start_thread+6)

 

  帶symbols的so文件

  對於比如手機公司的開發人員來說,一般來說出問題的so對應的帶symbols的so都在out/target/product/<model_name>/symbols/system/lib/下面,而對於常見的使用AndroidStudio開發的單個應用來說,其對應的帶symbols的在<PROJECT_ROOT>\app\src\main\obj\local\<ABI>\下面的so,而不能是\app\src\main\libs\<ABI>的,這里面的是不包含symbols信息的,拿這個去分析,輸出的結果就是“??:?”。其實這兩個so的體積對比也是很明顯的的,在我的應用中,前一個帶symbols的so的體積為7M多,而后一個只有2M。

 

  分析工具

  •   addr2line:用來分析單個pc地址對應的源碼行數,比如示例log中的第13行中的#00 pc 0004097c,0004097c就是crash時pc調用的堆棧地址,用這個地址就可以分析出對應在源碼中的行數;
  •   objdump:用來把相應的so變成匯編語言的asm文件,然后根據地址信息(比如0004097c)就可以找到更加詳細的相關函數信息;
  •   ndk-stack:用來把log信息全部翻譯成更加詳細的帶源碼行數信息的log,相當於是在整個crash堆棧信息都執行addr2line命令。

  對於使用linux系統作為開發環境的,linux就自帶addr2line命令。而對於筆者這種使用Windows的,在sdk中安裝了NDK之后,在ndk中就帶有這些工具。

  比如addr2line工具在:sdk\ndk-bundle\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin下面,同時這個bin下面包含很多其他工具,比如objdump,readelf等;

  ndk-stack工具則在sdk\ndk-bundle下面;

  關於這些工具的具體使用,在https://www.oschina.net/question/2241352_213433這篇文章中講的很詳細,我也就不再重復。

  但是提醒一點:crash log與對應的so一定要對應起來。即錯誤的情況是:你拿了一份舊的log,然后你修改了so相關的源碼,然后編譯出來了新的so,你拿着這個新的so以及舊log中的地址去讓addr2line等分析,那肯定是是得不到正確的結果的。

 

 

 

  剛剛提到的那篇文章講的很詳細,為了避免以后找不到所以我就把它復制到這里。

/****************************************************************************************************************************************/

轉自:https://www.oschina.net/question/2241352_213433

 

Android NDK是什么,為什么我們要用NDK?

Android NDK 是在SDK前面又加上了“原生”二字,即Native Development Kit,因此又被Google稱為“NDK”。眾所周知,Android程序運行在Dalvik虛擬機中,NDK允許用戶使用類似C / C++之類的原生代碼語言執行部分程序。NDK包括了:

  • 從C / C++生成原生代碼庫所需要的工具和build files。
  • 將一致的原生庫嵌入可以在Android設備上部署的應用程序包文件(application packages files,即.apk文件)中。
  • 支持所有未來Android平台的一些列原生系統頭文件和庫

為何要用到NDK?概括來說主要分為以下幾種情況:

  • 代碼的保護,由於apk的java層代碼很容易被反編譯,而C/C++庫反匯難度較大。
  • 在NDK中調用第三方C/C++庫,因為大部分的開源庫都是用C/C++代碼編寫的。
  • 便於移植,用C/C++寫的庫可以方便在其他的嵌入式平台上再次使用。

Android JNI是什么?和NDK是什么關系?

Java Native Interface(JNI)標准是java平台的一部分,它允許Java代碼和其他語言寫的代碼進行交互。JNI是本地編程接口,它使得在 Java 虛擬機(VM) 內部運行的 Java 代碼能夠與用其它編程語言(如 C、C++和匯編語言)編寫的應用程序和庫進行交互操作。

簡單來說,可以認為NDK就是能夠方便快捷開發.so文件的工具。JNI的過程比較復雜,生成.so需要大量操作,而NDK就是簡化了這個過程。

NDK的異常會不會導致程序Crash,NDK的常見的有哪些類型異常?

NDK編譯生成的.so文件作為程序的一部分,在運行發生異常時同樣會造成程序崩潰。不同於Java代碼異常造成的程序崩潰,在NDK的異常發生時,程序在Android設備上都會立即退出,即通常所說的閃退,而不會彈出“程序xxx無響應,是否立即關閉”之類的提示框。

NDK是使用C/C++來進行開發的,熟悉C/C++的程序員都知道,指針和內存管理是最重要也是最容易出問題的地方,稍有不慎就會遇到諸如內存無效訪問、無效對象、內存泄露、堆棧溢出等常見的問題,最后都是同一個結果:程序崩潰。例如我們常說的空指針錯誤,就是當一個內存指針被置為空(NULL)之后再次對其進行訪問;另外一個經常出現的錯誤是,在程序的某個位置釋放了某個內存空間,而后在程序的其他位置試圖訪問該內存地址,這就會產生一個無效地址錯誤。常見的錯誤類型如下:

  • 初始化錯誤
  • 訪問錯誤
    • 數組索引訪問越界
    • 指針對象訪問越界
    • 訪問空指針對象
    • 訪問無效指針對象
    • 迭代器訪問越界
  • 內存泄露
  • 參數錯誤
  • 堆棧溢出
  • 類型轉換錯誤
  • 數字除0錯誤

NDK錯誤發生時,我們能拿到什么信息?

利用Android NDK開發本地應用的時候,幾乎所有的程序員都遇到過程序崩潰的問題,但它的崩潰會在logcat中打印一堆看起來類似天書的堆棧信息,讓人舉足無措。單靠添加一行行的打印信息來定位錯誤代碼做在的行數,無疑是一件令人崩潰的事情。在網上搜索“Android NDK崩潰”,可以搜索到很多文章來介紹如何通過Android提供的工具來查找和定位NDK的錯誤,但大都晦澀難懂。下面以一個實際的例子來說明,首先生成一個錯誤,然后演示如何通過兩種不同的方法,來定位錯誤的函數名和代碼行。

首先,看我們在hello-jni程序的代碼中做了什么(有關如何創建或導入工程,此處略),看下圖:在JNI_OnLoad()的函數中,即so加載時,調用willCrash()函數,而在willCrash()函數中, std::string的這種賦值方法會產生一個空指針錯誤。這樣,在hello-jni程序加載時就會閃退。我們記一下這兩個行數:在61行調用了willCrash()函數;在69行發生了崩潰。

下面來看看發生崩潰(閃退)時系統打印的logcat日志:

[plain]   view plain  copy
 
  1. *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***  
  2.  Build fingerprint: 'vivo/bbk89_cmcc_jb2/bbk89_cmcc_jb2:4.2.1/JOP40D/1372668680:user/test-keys'  
  3.  pid: 32607, tid: 32607, name: xample.hellojni  >>> com.example.hellojni <<<  
  4.  signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000  
  5.      r0 00000000  r1 beb123a8  r2 80808080  r3 00000000  
  6.      r4 5d635f68  r5 5cdc3198  r6 41efcb18  r7 5d62df44  
  7.      r8 4121b0c0  r9 00000001  sl 00000000  fp beb1238c  
  8.      ip 5d635f7c  sp beb12380  lr 5d62ddec  pc 400e7438  cpsr 60000010  
  9.    
  10.  backtrace:  
  11.      #00  pc 00023438  /system/lib/libc.so   
  12.      #01  pc 00004de8  /data/app-lib/com.example.hellojni-2/libhello-jni.so  
  13.      #02  pc 000056c8  /data/app-lib/com.example.hellojni-2/libhello-jni.so  
  14.      #03  pc 00004fb4  /data/app-lib/com.example.hellojni-2/libhello-jni.so  
  15.      #04  pc 00004f58  /data/app-lib/com.example.hellojni-2/libhello-jni.so  
  16.      #05  pc 000505b9  /system/lib/libdvm.so  
  17.      #06  pc 00068005  /system/lib/libdvm.so  
  18.      #07  pc 000278a0  /system/lib/libdvm.so  
  19.      #08  pc 0002b7fc  /system/lib/libdvm.so  
  20.      #09  pc 00060fe1  /system/lib/libdvm.so  
  21.      #10  pc 0006100b  /system/lib/libdvm.so  
  22.      #11  pc 0006c6eb  /system/lib/libdvm.so  
  23.      #12  pc 00067a1f  /system/lib/libdvm.so  
  24.      #13  pc 000278a0  /system/lib/libdvm.so  
  25.      #14  pc 0002b7fc  /system/lib/libdvm.so  
  26.      #15  pc 00061307  /system/lib/libdvm.so  
  27.      #16  pc 0006912d  /system/lib/libdvm.so  
  28.      #17  pc 000278a0  /system/lib/libdvm.so  
  29.      #18  pc 0002b7fc  /system/lib/libdvm.so  
  30.      #19  pc 00060fe1  /system/lib/libdvm.so  
  31.      #20  pc 00049ff9  /system/lib/libdvm.so  
  32.      #21  pc 0004d419  /system/lib/libandroid_runtime.so  
  33.      #22  pc 0004e1bd  /system/lib/libandroid_runtime.so  
  34.      #23  pc 00001d37  /system/bin/app_process  
  35.      #24  pc 0001bd98  /system/lib/libc.so  
  36.      #25  pc 00001904  /system/bin/app_process  
  37.    
  38.  stack:  
  39.           beb12340  012153f8    
  40.           beb12344  00054290    
  41.           beb12348  00000035    
  42.           beb1234c  beb123c0  [stack]  
  43.        
  44. ……  

 

如果你看過logcat打印的NDK錯誤時的日志就會知道,我省略了后面很多的內容,很多人看到這么多密密麻麻的日志就已經頭暈腦脹了,即使是很多資深的Android開發者,在面對NDK日志時也大都默默的選擇了無視。

 

“符號化”NDK錯誤信息的方法

其實,只要你細心的查看,再配合Google 提供的工具,完全可以快速的准確定位出錯的代碼位置,這個工作我們稱之為“符號化”。需要注意的是,如果要對NDK錯誤進行符號化的工作,需要保留編譯過程中產生的包含符號表的so文件,這些文件一般保存在$PROJECT_PATH/obj/local/目錄下。

第一種方法:ndk-stack

這個命令行工具包含在NDK工具的安裝目錄,和ndk-build和其他一些常用的NDK命令放在一起,比如在我的電腦上,其位置是/android-ndk-r9d/ndk-stack。根據Google官方文檔,NDK從r6版本開始提供ndk-stack命令,如果你用的之前的版本,建議還是盡快升級至最新的版本。使用ndk –stack命令也有兩種方式

使用ndk-stack實時分析日志

在運行程序的同時,使用adb獲取logcat日志,並通過管道符輸出給ndk-stack,同時需要指定包含符號表的so文件位置;如果你的程序包含了多種CPU架構,在這里需求根據錯誤發生時的手機CPU類型,選擇不同的CPU架構目錄,如:

[plain]   view plain  copy
 
  1. adb shell logcat | ndk-stack -sym $PROJECT_PATH/obj/local/armeabi  

當崩潰發生時,會得到如下的信息:

[plain]   view plain  copy
 
  1. ********** Crash dump: **********  
  2. Build fingerprint: 'vivo/bbk89_cmcc_jb2/bbk89_cmcc_jb2:4.2.1/JOP40D/1372668680:user/test-keys'  
  3. pid: 32607, tid: 32607, name: xample.hellojni  >>> com.example.hellojni <<<  
  4. signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000  
  5. Stack frame #00  pc 00023438  /system/lib/libc.so (strlen+72)  
  6. Stack frame #01  pc 00004de8  /data/app-lib/com.example.hellojni-2/libhello-jni.so (std::char_traits<char>::length(char const*)+20): Routine std::char_traits<char>::length(char const*) at /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/char_traits.h:229  
  7. Stack frame #02  pc 000056c8  /data/app-lib/com.example.hellojni-2/libhello-jni.so (std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)+44): Routine basic_string at /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/_string.c:639  
  8. Stack frame #03  pc 00004fb4  /data/app-lib/com.example.hellojni-2/libhello-jni.so (willCrash()+68): Routine willCrash() at /home/testin/hello-jni/jni/hello-jni.cpp:69  
  9. Stack frame #04  pc 00004f58  /data/app-lib/com.example.hellojni-2/libhello-jni.so (JNI_OnLoad+20): Routine JNI_OnLoad at /home/testin/hello-jni/jni/hello-jni.cpp:61  
  10. Stack frame #05  pc 000505b9  /system/lib/libdvm.so (dvmLoadNativeCode(char const*, Object*, char**)+516)  
  11. Stack frame #06  pc 00068005  /system/lib/libdvm.so  
  12. Stack frame #07  pc 000278a0  /system/lib/libdvm.so  
  13. Stack frame #08  pc 0002b7fc  /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+180)  
  14. Stack frame #09  pc 00060fe1  /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+272)  
  15. ……(后面略)  

我們重點看一下#03和#04,這兩行都是在我們自己生成的libhello-jni.so中的報錯信息,那么會發現如下關鍵信息:

[plain]   view plain  copy
 
  1. #03 (willCrash()+68): Routine willCrash() at /home/testin/hello-jni/jni/hello-jni.cpp:69  
  2. #04 (JNI_OnLoad+20): Routine JNI_OnLoad at /home/testin/hello-jni/jni/hello-jni.cpp:61  

回想一下我們的代碼,在JNI_OnLoad()函數中(第61行),我們調用了willCrash()函數;在willCrash()函數中(第69行),我們制造了一個錯誤。這些信息都被准確無誤的提取了出來!是不是非常簡單?

先獲取日志,再使用ndk-stack分析

這種方法其實和上面的方法沒有什么大的區別,僅僅是logcat日志獲取的方式不同。可以在程序運行的過程中將logcat日志保存到一個文件,甚至可以在崩潰發生時,快速的將logcat日志保存起來,然后再進行分析,比上面的方法稍微靈活一點,而且日志可以留待以后繼續分析。

[plain]   view plain  copy
 
  1. adb shell logcat > 1.log  
  2. ndk-stack -sym $PROJECT_PATH/obj/local/armeabi –dump 1.log  

第二種方法:使用addr2line和objdump命令

這個方法適用於那些,不滿足於上述ndk-stack的簡單用法,而喜歡刨根問底的程序員們,這兩個方法可以揭示ndk-stack命令的工作原理是什么,盡管用起來稍微麻煩一點,但是可以滿足一下程序員的好奇心。

先簡單說一下這兩個命令,在絕大部分的linux發行版本中都能找到他們,如果你的操作系統是linux,而你測試手機使用的是Intel x86系列,那么你使用系統中自帶的命令就可以了。然而,如果僅僅是這樣,那么絕大多數人要絕望了,因為恰恰大部分開發者使用的是Windows,而手機很有可能是armeabi系列。

別急,在NDK中自帶了適用於各個操作系統和CPU架構的工具鏈,其中就包含了這兩個命令,只不過名字稍有變化,你可以在NDK目錄的toolchains目錄下找到他們。以我的Mac電腦為例,如果我要找的是適用於armeabi架構的工具,那么他們分別為arm-linux-androideabi-addr2line和arm-linux-androideabi-objdump;位置在下面目錄中,后續介紹中將省略此位置:

[plain]   view plain  copy
 
  1. /Developer/android_sdk/android-ndk-r9d/toolchains/arm-linux-androideabi-4.8/prebuilt/darwin-x86_64/bin/  

假設你的電腦是windows, CPU架構為mips,那么你要的工具可能包含在這個目錄中:

[plain]   view plain  copy
 
  1. D:\ android-ndk-r9d\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64\bin\  

 

好了言歸正傳,如何使用這兩個工具,下面具體介紹:

1. 找到日志中的關鍵函數指針

其實很簡單,就是找到backtrace信息中,屬於我們自己的so文件報錯的行。

首先要找到backtrace信息,有的手機會明確打印一行backtrace(比如我們這次使用的手機),那么這一行下面的一系列以“#兩位數字 pc”開頭的行就是backtrace信息了。有時可能有的手機並不會打印一行backtrace,那么只要找到一段以“#兩位數字 pc ”開頭的行,就可以了。

其次要找到屬於自己的so文件報錯的行,這就比較簡單了。找到這些行之后,記下這些行中的函數地址

2. 使用addr2line查找代碼位置

執行如下的命令,多個指針地址可以在一個命令中帶入,以空格隔開即可

[plain]   view plain  copy
 
  1. arm-linux-androideabi-addr2line –e obj/local/armeabi/libhello-jni.so 00004de8 000056c8 00004fb4 00004f58  

結果如下
[plain]   view plain  copy
 
  1. /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/char_traits.h:229  
  2. /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/_string.c:639  
  3. /WordSpaces/hello-jni/jni/hello-jni.cpp:69  
  4. /WordSpaces hello-jni/jni/hello-jni.cpp:6  

從addr2line的結果就能看到,我們拿到了我們自己的錯誤代碼的調用關系和行數,在hello-jni.cpp的69行和61行(另外兩行因為使用的是標准函數,可以忽略掉),結果和ndk-stack是一致的,說明ndk-stack也是通過addr2line來獲取代碼位置的。

3. 使用objdump獲取函數信息

通過addr2line命令,其實我們已經找到了我們代碼中出錯的位置,已經可以幫助程序員定位問題所在了。但是,這個方法只能獲取代碼行數,並沒有顯示函數信息,顯得不那么“完美”,對於追求極致的程序員來說,這當然是不夠的。下面我們就演示怎么來定位函數信息。

 

使用如下命令導出函數表:

[plain]   view plain  copy
 
  1. arm-linux-androideabi-objdump –S obj/local/armeabi/libhello-jni.so > hello.asm  

 

在生成的asm文件中查找剛剛我們定位的兩個關鍵指針00004fb4和00004f58

 

 

從這兩張圖可以清楚的看到(要注意的是,在不同的NDK版本和不同的操作系統中,asm文件的格式不是完全相同,但都大同小異,請大家仔細比對),這兩個指針分別屬於willCrash()和JNI_OnLoad()函數,再結合剛才addr2line的結果,那么這兩個地址分別對應的信息就是:

[plain]   view plain  copy
 
  1. 00004fb4: willCrash() /WordSpaces/hello-jni/jni/hello-jni.cpp:69  
  2. 00004f58: JNI_OnLoad()/WordSpaces/hello-jni/jni/hello-jni.cpp:61  

 

相當完美,和ndk-stack得到的信息完全一致!

 

使用Testin崩潰分析服務定位NDK錯誤

以上提到的方法,只適合在開發測試期間,如果你的應用或者游戲已經發布上線,而用戶經常反饋說崩潰、閃退,指望用戶幫你收集信息定位問題,幾乎是不可能的。這個時候,我們就需要用其他的手段來捕獲崩潰信息。

目前業界已經有一些公司推出了崩潰信息收集的服務,通過嵌入SDK,在程序發生崩潰時收集堆棧信息,發送到雲服務平台,從而幫助開發者定位錯誤信息。在這方面,處於領先地位的是國內的Testin和國外的crittercism,其中crittercism需要付費,而且沒有專門的中國開發者支持,我們更推薦Testin,其崩潰分析服務是完全免費的。

Testin從1.4版本開始支持NDK的崩潰分析,其最新版本已經升級到1.7。當程序發生NDK錯誤時,其內嵌的SDK會收集程序在用戶手機上發生崩潰時的堆棧信息(主要就是上面我們通過logcat日志獲取到的函數指針)、設備信息、線程信息等等,SDK將這些信息上報至Testin雲服務平台,只要登陸到Testin平台,就可以看到所有用戶上報的崩潰信息,包括NDK;並且這些崩潰做過歸一化的處理,在不同系統和ROM的版本上打印的信息會略有不同,但是在Testin的網站上這些都做了很好的處理,避免了我們一些重復勞動。

 

 

上圖的紅框部分,就是從用戶手機上報的,我們自己的so中報錯的函數指針地址堆棧信息,就和我們開發時從logcat讀到的日志一樣,是一些晦澀難懂的指針地址,Testin為NDK崩潰提供了符號化的功能,只要將我們編譯過程中產生的包含符號表的so文件上傳(上文我們提到過的obj/local/目錄下的適用於各個CPU架構的so),就可以自動將函數指針地址定位到函數名稱和代碼行數。符號化之后,看起來就和我們前面在本地測試的結果是一樣的了,一目了然。

而且使用這個功能還有一個好處:這些包含符號表的so文件,在每次我們自己編譯之后都會改變,很有可能我們剛剛發布一個新版本,這些目錄下的so就已經變了,因為開發者會程序的修改程序;在這樣的情況下,即使我們拿到了崩潰時的堆棧信息,那也無法再進行符號化了。所以我們在編譯打包完成后記得備份我們的so文件。這時我們可以將這些文件上傳到Testin進行符號化的工作,Testin會為我們保存和管理不同版本的so文件,確保信息不會丟失。來看一下符號化之后的顯示:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM