在Android應用crash的類型中,native類型crash應該是比較難的一種了,因為大家接觸的少,然後相對也要多轉幾道工序,所有大部分對這個都比較生疏。雖然相關文章也有很多了,但是我在剛開始學的過程中還是遇到一些問題,下麵一一記錄,以便將來翻閱。 分析native crash 日誌需要幾個東 ...
在Android應用crash的類型中,native類型crash應該是比較難的一種了,因為大家接觸的少,然後相對也要多轉幾道工序,所有大部分對這個都比較生疏。雖然相關文章也有很多了,但是我在剛開始學的過程中還是遇到一些問題,下麵一一記錄,以便將來翻閱。
分析native crash 日誌需要幾個東西:
- addr2line,objdump,ndk-stack等幾個工具
- 帶symbols的so文件
- 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- *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
- Build fingerprint: 'vivo/bbk89_cmcc_jb2/bbk89_cmcc_jb2:4.2.1/JOP40D/1372668680:user/test-keys'
- pid: 32607, tid: 32607, name: xample.hellojni >>> com.example.hellojni <<<
- signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000
- r0 00000000 r1 beb123a8 r2 80808080 r3 00000000
- r4 5d635f68 r5 5cdc3198 r6 41efcb18 r7 5d62df44
- r8 4121b0c0 r9 00000001 sl 00000000 fp beb1238c
- ip 5d635f7c sp beb12380 lr 5d62ddec pc 400e7438 cpsr 60000010
- backtrace:
- #00 pc 00023438 /system/lib/libc.so
- #01 pc 00004de8 /data/app-lib/com.example.hellojni-2/libhello-jni.so
- #02 pc 000056c8 /data/app-lib/com.example.hellojni-2/libhello-jni.so
- #03 pc 00004fb4 /data/app-lib/com.example.hellojni-2/libhello-jni.so
- #04 pc 00004f58 /data/app-lib/com.example.hellojni-2/libhello-jni.so
- #05 pc 000505b9 /system/lib/libdvm.so
- #06 pc 00068005 /system/lib/libdvm.so
- #07 pc 000278a0 /system/lib/libdvm.so
- #08 pc 0002b7fc /system/lib/libdvm.so
- #09 pc 00060fe1 /system/lib/libdvm.so
- #10 pc 0006100b /system/lib/libdvm.so
- #11 pc 0006c6eb /system/lib/libdvm.so
- #12 pc 00067a1f /system/lib/libdvm.so
- #13 pc 000278a0 /system/lib/libdvm.so
- #14 pc 0002b7fc /system/lib/libdvm.so
- #15 pc 00061307 /system/lib/libdvm.so
- #16 pc 0006912d /system/lib/libdvm.so
- #17 pc 000278a0 /system/lib/libdvm.so
- #18 pc 0002b7fc /system/lib/libdvm.so
- #19 pc 00060fe1 /system/lib/libdvm.so
- #20 pc 00049ff9 /system/lib/libdvm.so
- #21 pc 0004d419 /system/lib/libandroid_runtime.so
- #22 pc 0004e1bd /system/lib/libandroid_runtime.so
- #23 pc 00001d37 /system/bin/app_process
- #24 pc 0001bd98 /system/lib/libc.so
- #25 pc 00001904 /system/bin/app_process
- stack:
- beb12340 012153f8
- beb12344 00054290
- beb12348 00000035
- beb1234c beb123c0 [stack]
- ……
如果你看過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- adb shell logcat | ndk-stack -sym $PROJECT_PATH/obj/local/armeabi
當崩潰發生時,會得到如下的信息:
[plain] view plain copy- ********** Crash dump: **********
- Build fingerprint: 'vivo/bbk89_cmcc_jb2/bbk89_cmcc_jb2:4.2.1/JOP40D/1372668680:user/test-keys'
- pid: 32607, tid: 32607, name: xample.hellojni >>> com.example.hellojni <<<
- signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000
- Stack frame #00 pc 00023438 /system/lib/libc.so (strlen+72)
- 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
- 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
- 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
- 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
- Stack frame #05 pc 000505b9 /system/lib/libdvm.so (dvmLoadNativeCode(char const*, Object*, char**)+516)
- Stack frame #06 pc 00068005 /system/lib/libdvm.so
- Stack frame #07 pc 000278a0 /system/lib/libdvm.so
- Stack frame #08 pc 0002b7fc /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+180)
- Stack frame #09 pc 00060fe1 /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+272)
- ……(後面略)
我們重點看一下#03和#04,這兩行都是在我們自己生成的libhello-jni.so中的報錯信息,那麼會發現如下關鍵信息:
[plain] view plain copy- #03 (willCrash()+68): Routine willCrash() at /home/testin/hello-jni/jni/hello-jni.cpp:69
- #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- adb shell logcat > 1.log
- 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- /Developer/android_sdk/android-ndk-r9d/toolchains/arm-linux-androideabi-4.8/prebuilt/darwin-x86_64/bin/
假設你的電腦是windows, CPU架構為mips,那麼你要的工具可能包含在這個目錄中:
[plain] view plain copy- 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- arm-linux-androideabi-addr2line –e obj/local/armeabi/libhello-jni.so 00004de8 000056c8 00004fb4 00004f58
結果如下 [plain] view plain copy
- /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/char_traits.h:229
- /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/_string.c:639
- /WordSpaces/hello-jni/jni/hello-jni.cpp:69
- /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- 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- 00004fb4: willCrash() /WordSpaces/hello-jni/jni/hello-jni.cpp:69
- 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文件,確保信息不會丟失。來看一下符號化之後的顯示: