NDK crash栈信息的错误定位
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文件,確保信息不會丟失。來看一下符號化之后的顯示:
總結
以上是生活随笔為你收集整理的NDK crash栈信息的错误定位的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 那些伤那些痛是什么歌呢?
- 下一篇: android mediaplayer状