作者簡介:dc, 天天P圖AND工程師
Android上比較常見的問題除了ANR、Java Crash還有Native Crash,尤其是像天天P圖這樣的具備拍攝能力的APP,使用了大量native代碼進行性能提升。Native Crash常常發生在帶有Jni代碼的APP中,或者系統的Native服務中。作為比較難分析的一類問題,Native Crash其實還是有較多的方法去定位。
1. 為什麼會産生Native Crash?
常見導緻Native Crash的原因有以下幾種:
1. jni内部數組越界、緩沖區溢出、空指針、野指針等;
2. jni中多線程出現競争,比如一個線程調用jni接口釋放了内部一個指針,另一個線程調用另外一個jni接口還在使用這個指針;
3. Android ART發現或出現異常;
4. 其他framework、Kernel或硬體bug;
2. Native Crash日志長什麼樣?
一個典型的Native Crash日志如下:
對于不同的機器以及不同的系統,除了列印出的native的調用棧,還可能出現Java調用棧,或者諸如.../base.odex之類的資訊。
其中如果出現libart.so(比如上圖),不要簡單的認為Runtime出現異常,實際上是因為在Java的代碼執行過程中,需要Runtime參與方法查找、方法Invoke等操作,是以棧中存在art的資訊也是正常的。
另外如果出現base.odex,或者/data/dalvik-cache/arm64/system@[email protected],是因為Runtime對apk或者framework的dex進行了dex2oat的優化,直接執行機器碼。盡管出現這些資訊的時候,一般會沒有Java的調用棧,但是如果手機可以root,也可以通過oat檔案、PC位址、函數偏移量查找到對應的代碼。這裡涉及的知識暫時不做贅述。
3. Native Crash的類型
從常見的調用棧中,我們也可以看到Native Crash的一般類型:
1. Abort:Abort一般是Runtime通過libc主動進行的中止操作;
2. 空指針解引用:Jni代碼出現空指針;
3. 低位址解引用:一般是結構體指針出現空指針,通路内部變量的偏移位址;
4. 棧破壞:記憶體越界、緩沖區溢出等;
5. 其他:多線程或者其他原因導緻。
除了Jni代碼可能導緻Native Crash,系統的native程序或者服務以及dex編譯生成的機器碼oat也都可能以為缺陷出現Crash,表現也是Native Crash。
一旦出現Native Crash,系統或者Runtime産生對應的信号,然後通過對應的信号處理函數進行處理。
4. Android信号的處理機制
4.1 SignalCatcher
Android的Zygote在Fork程序的時候,都會在InitNonZygoteOrPostFork時調用StartSignalCatcher建立一個新的SignalCatcher線程,這個線程的作用就是用來捕獲Linux信号。
這個線程也是通過pthread_create建立,運作起來之後,會一直等待信号的到來:
以上代碼可以看出,隻處理兩種類型的信号,一種是SIGQUIT,一種是SIGUSR1。
對于SIGUSR1,也就是kill -10産生的信号,Runtime會強制進行GC并儲存GC的profile資訊。
對于SIGQUIT,即kill -3産生的信号,Runtime則會dump fingerprint、ABI、Build type、線程調用棧、GC profile、虛拟記憶體資訊/proc/self/maps等,并産生/data/anr/traces.txt檔案。
Linux中對信号的定義在signum.h檔案中:
4.2 FaultManager
除了SignalCatcher,Runtime在啟動的時候會建立一個FaultManager,
FaultManager則會捕獲更多真正意義上的信号(SIGABRT/SIGBUS/SIGFPE/SIGILL/SIGSEGV):
SIGABRT一般由Runtime通過調用Runtime::Abort主動發起,一般出現在Jni中參數異常或者Runtime内部出現特定已知問題的時候,比如Runtime中調用LOG(FATAL)時都會調用到Runtime::Abort産生SIGABRT信号:
其他的信号一般原因是:
1. SIGBUS:總線出錯,比如資料對齊;
2. SIGFPE:錯誤的運算操作,比如除零;
3. SIGILL:出現了非法指令;
4. SIGSEGV:通路了一個不合法記憶體位址,空指針或者記憶體越界導緻的。
4.3 SignalChain
實際上FaultManager隻對SIGSEGV進行了特殊處理,如果處理不了,也會通過art_sigsegv_fault再交給普通的sigaction進行處理,這樣做的原因是,Java中的StackOverFlow以及NullPointerException異常是通過SIGSEGV實作的,如果出現了這些異常,需要先列印出Java調用棧,是以會執行特殊的信号處理函數。
另外對于這幾種信号,Runtime在啟動時候就通過InitPlatformSignalHandlersCommon注冊了信号處理的handler:
如果出現了以上信号,會調用HandleUnexpectedSignalCommon進行處理,處理方式就是列印一些必要的調試資訊,包括平台資訊、程序線程資訊、寄存器資訊以及線程調用棧、虛拟記憶體資訊等(這些資訊除了能在logcat中看到以外,還能在手機的/data/tombstones/中看到):
5. 如何分析Native Crash?
5.1 logcat
分析Native Crash最直接的方式是檢視logcat日志,一般情況下,隻要APP沒有自己實作信号捕獲機制(比如使用了Bugly插件或者google breakpad),就不會影響到Runtime正常列印調用棧。我們通常隻需要執行
adb logcat|grep DEBUG
複制
就能過濾出Native Crash的日志,比如:
從日志中,我們可以看到以下資訊(以上圖為例):
1. 手機的型号:HUAWEI/VTR-AL00/HWVTR;
2. Android系統版本:8.0.0
3. 系統的build号:358(C00)
4. 系統的類型:user(對應有user/eng/userdebug/optional等,user表示是release版本,eng是調試版本)
5. ABI資訊:arm
6. Crash的程序号:4090
7. Crash的線程号:4489
8. Crash的線程名稱:GLThread 23038(名稱可能被裁減導緻不全)
9. Crash的程序名稱:com.tencent.ptuxffectssdkdemo
10. 信号名:signal 11 (SIGSEGV)
11. 信号産生的原因:code 2 (SEGV_ACCERR)(如果信号是SIGABRT,則對應原因可能是SI_USER/SI_QUEUE/SI_TKILL/SI_KERNEL,其中SI_TKILL表示程式使用tkill發出的信号,如果是SI_USER,表示是使用者手動發起的信号,比如使用指令kill -3殺死程序)
12. 異常的記憶體位址:fault addr 0xc002c85c(如果是SIGSEGV/SIGBUS等信号,一般都會有異常記憶體位址顯示)
13. 中止消息:無(如果是SIGABRT,可能有類似java_vm_ext.cc:504] JNI DETECTED ERROR IN APPLICATION: java_array == null這樣的消息)
14. 寄存器指向的記憶體位址:
r0 bca89e40 r1 bca89e40 r2 c002c85c r3 0002e8b0r4 00000000 r5 e6d352f0 r6 c1b1323c r7 c1b13110r8 00000056 r9 c589bc00 sl c1b13240 fp c589bc00ip 00000002 sp c1b13000 lr bfef0c81 pc c002c85c cpsr 60070010
15. Native調用棧:
#00 pc 0000285c /data/app/com.tencent.ptuxffectssdkdemo-LgQTLgNk9enAtLgqXLJJ_g==/lib/arm/libgameplay.so (offset 0x2e0000)
......
另外,對于寄存器的描述,可以參考《Procedure Call Standard for the ARM® Architecture》以及《Procedure Call Standard for the ARM 64-bit Architecture (AArch64)》:
以上這些寄存器對于我們分析函數參數傳遞等具有重要的意義。
如果發現由于使用了Bugly等插件導緻無法正常列印出這些資訊,那麼建議關閉這些插件再複現問題。
5.2 tombstone檔案
當然,如果你的手機有root權限,可以pull出tombstone檔案,目錄在/data/tombstones/。tombstone檔案是在出現Native Crash時的崩潰轉儲檔案,一般最多儲存10個檔案,如果有新的Crash則會覆寫掉舊的檔案。
tombstone檔案相比logcat能提供更為豐富的調試資訊,比如棧記憶體dump,寄存器指向記憶體位址周圍的記憶體dump,以及從/prop/<pid>/maps/中cat出的虛拟記憶體資訊。這些資訊對于調試記憶體問題尤為重要。
一個典型的寄存器r5(r5 9c083d20)指向記憶體位址附近記憶體dump如下:
關于tombstone檔案的詳細解讀,可以參考:https://source.android.com/devices/tech/debug/native-crash#crashdump
分析tombstone檔案時,需要注意一點是,如果是SIGABRT信号,一般會有一條Abort Message,這條資訊基本上可以說明該問題出現的原因,比如jni參數空指針之類(SIGABRT信号一般出現在assert失敗時産生的Crash中)。
5.3 Native調用棧分析
分析Native Crash最關鍵的是看調用棧,一個有效的調用棧可以直接定位到問題出現的現場,當然也不排除調用棧對應不上問題現場的現象。
Native調用棧的每一條都由幾部分組成,以#00 pc 0004b3ac /system/lib/libc.so (tgkill+12)為例:
1. 棧幀号:#00
2. pc位址值:pc 0004b3ac
3. 對應的虛拟記憶體映射區域名稱(通常是共享庫或可執行檔案):/system/lib/libc.so
4. PC 值對應的符号:tgkill
5. 符号偏移量(以位元組為機關):12
由于app中的so是通過jni代碼編譯而來,編譯出的so如果有對應的調試資訊,就可以通過這些調試資訊找到符号對應的代碼行,這些調試資訊就是符号表,包括symtab以及debug相關的section。
對于一個so,不同的資訊以section節的方式組織,通過arm-linux-androideabi-readelf -S <so-file-name>可以看到該section資訊。
以下是一個典型的沒有symtab符号表的so資訊(這個so是經過執行gradle任務transformNativeLibsWithStripDebugSymbolForDebug時strip後的):
而下面這個則是帶有符号表的so資訊:
正常情況下,cmake編譯的so是分為兩種,一個是libs下的不帶符号表的so,一個是objs下面帶有符号表的so,調試的時候需要用到objs下面的檔案。盡管可以将帶符号表的so放到lib/armeabi下面進行打包,但是因為打包apk時會自動執行transformNativeLibsWithStripDebugSymbolForDebug這樣的gradle任務,最終這些調試資訊會在打包apk strip掉,可以在gradle中增加以下選項禁止strip:
packagingOptions{
doNotStrip "*/*/*.so"
}
複制
有了帶符号表的so,我們可以使用addr2line工具從調用棧找到對應代碼行:
arm-linux-androideabi-addr2line -Cpfie <symbol-so-path> <pc-address>
複制
比如,對于調用棧:
#01 pc 001a7c7f /data/app/com.tencent.ptuxffectssdkdemo-LgQTLgNk9enAtLgqXLJJ_g==/lib/arm/libgameplay.so (_ZN8gameplay14PituCameraGame10initializeEv+1298)
它的符号(函數名稱)為_ZN8gameplay14PituCameraGame10initializeEv(使用
arm-linux-androideabi-c++filt _ZN8gameplay14PituCameraGame10initializeEv
從簽名還原之後為gameplay::PituCameraGame::initialize()),函數内部偏移位址為十進制1298,pc位址為十六進制001a7c7f:
如果出現無法解析的現象,可能是因為目前符号表so與實際出現Crash的so不比對(比如使用新代碼編譯的帶符号表的so)。出現這樣的現象時,對于一種情況,仍然可以進行解析,即確定目前出問題的native函數沒有進行過修改,代碼内部偏移量仍然有效。首先使用
arm-linux-androideabi-nm -D module_video/libs/armeabi/libgameplay.so | grep _ZN8gameplay14PituCameraGame10initializeEv
查找到符号起始位址,然後再加上調用棧中的偏移量(比如上面例子中的1298),然後将新的位址給addr2line進行解析。
另外,Android為了簡化addr2line解析整個Crash全部調用棧的過程,提供了ndk-stack腳本工具批量處理,有興趣可以看下它的Python源碼:
如果我們的調用棧中出現了app對應的odex/oat檔案,則可以導出oat并使用objdump工具查找到對應的java代碼。這個過程需要分析編譯器從dex生成的彙編機器碼,然後根據一定規則映射到dalvik位元組碼的指令偏移上,進而找到對應的Native代碼的Java調用棧,這裡以後有空再介紹。
6. Native Crash調試方法
6.1 gdb調試
新版的Android Studio支援直接建立帶有Native代碼的工程,并使用cmake編譯jni代碼,内部使用llvm+lldb進行編譯和調試。盡管Android Studio預設不使用gdb進行調試,我們仍然可以使用gdb對我們的native代碼進行調試,因為gdb是一款優秀的調試工具,尤其是對于我們的native源碼單獨進行編譯,與java工程不一起管理的時候,除非我們能輕易将native代碼放到Android Studio進行cmake編譯。
在Android上使用gdb編譯不是一件輕松的事情,但是也并不複雜。Android SDK中實際上已經包含了一套gdb調試工具,我們直接拿來使用即可。首先你需要找到這些工具,包括gdb+gdbserver:
其中gdbserver是用在target(手機)中附加到程序進行調試的服務,而gdb則是host上用于調試的界面,或者叫做client,另外你還可以給gdb加上一個圖形界面。
完整的調試架構大緻如下:
下面我們看看如何讓gdb連接配接上的native代碼。步驟分為以下4部分:
1. 将gdbserver放入手機(注意gdbserver可執行程式的abi必須與app的abi一緻);
2. adb端口轉發;
3. 啟動調試器并attach到目标app程序;
4. 通過gdb連接配接remote的gdbserver開始調試。
如果你的手機已經root了,恭喜你,你可以少走一些彎路。對于root的手機(同時建議通過setenforce 0關閉selinux,防止安全設定禁用某些權限),以上4步可以具體為:
1. push gdbserver到手機:
adb push ndk-bundle/prebuilt/android-arm/gdbserver/gdbserver /data/local/tmp/gdbserver
,之後增加可執行權限:
adb shell chmod u+x /data/local/tmp/gdbserver
;
2. adb端口轉發:
adb forward tcp:6666 tcp:6666
3. attch到目标程序:
adb shell /data/local/tmp/gdbserver --attach <pid>
4. 使用host版的gdb連接配接gdbserver:
./ndk-bundle/prebuilt/darwin-x86_64/bin/gdb -tui
,然後輸入
target remote:6666
就可以愉快地開始調試了(這裡建議使用sdk中的gdb,而不要用系統的gdb,因為可能存在協定不一緻導緻gdb無法與gdbserver正常通信)。
另外root的手機可以直接将帶有符号表的so push到/data/app/<package-name>/lib/arm/下面替換,友善調試的時候gdb管理源代碼。而非root的手機就要使用之前提到的doNotStrip方式打包進apk進行調試了。
如果你的手機沒有root,那麼就可能遇到一堆無權限的問題,比如無權限執行gdbserver、無權限attach到程序、無權限建立socket進行通信等等;這裡通過參考Android Studio進行native調試的方法,可以順利進行調試。
先看看我們用Android Studio的lldb調試器進行native調試時的輸出:
$ adb shell cat /data/local/tmp/lldb-server | run-as com.tencent.weishi sh -c 'cat > /data/data/com.tencent.weishi/lldb/bin/lldb-server && chmod 700 /data/data/com.tencent.weishi/lldb/bin/lldb-server'$ adb shell cat /data/local/tmp/start_lldb_server.sh | run-as com.tencent.weishi sh -c 'cat > /data/data/com.tencent.weishi/lldb/bin/start_lldb_server.sh && chmod 700 /data/data/com.tencent.weishi/lldb/bin/start_lldb_server.sh'Starting LLDB server: /data/data/com.tencent.weishi/lldb/bin/start_lldb_server.sh /data/data/com.tencent.weishi/lldb unix-abstract /com.tencent.weishi-0 platform-1533822977589.sock "lldb process:gdb-remote packets"Debugger attached to process 12824
從上面可以看出,Android Studio通過cat輸出lldb-server并run-as以應用的權限執行cat進行接收,然後将lldb-server寫入到app的私有資料目錄,緊接着chmod 700增加可執行權限。然後使用同樣的方式将一個shell腳本start_lldb_server.sh發送到app資料目錄。最後以app的權限運作腳本啟動lldb。
這樣我們可以使用同樣的方式将gdbserver附加到調試程序:
1. push gdbserver到手機:先建立目錄
adb shell mkdir /data/local/tmp/
,然後push檔案:
adb push ndk-bundle/prebuilt/android-arm/gdbserver/gdbserver /data/local/tmp/gdbserver
;
2. 将gdbserver拷貝到調試app的私有資料目錄:
adb shell "cat /data/local/tmp/gdbserver | run-as <package-name> sh -c 'cat > /data/data/<package-name>/gdbserver && chmod 700 /data/data/<package-name>/gdbserver'"
3. adb端口轉發:由于非root手機沒有權限建立socket,可以轉發到localfilesystem上,方法如下:
adb forward tcp:6666 localfilesystem:/data/data/<package-name>/debug-pipe
4. 啟動gdbserver:
adb shell run-as <package-name> ./gdbserver +debug-pipe --attach <pid>
5. 使用host版的gdb連接配接gdbserver:
./ndk-bundle/prebuilt/darwin-x86_64/bin/gdb -tui
,然後輸入
target remote:6666
就可以愉快地開始調試了
這裡我将以上步驟寫成了腳本,效果如下:
之後調試界面如下:
還可以給gdb加上一個gui界面,比如基于浏覽器的gdbgui:
這樣我們就可以友善使用gdb進行各種調試了,比如檢視變量值、位址是否空指針等等。另外如果有權限,還可以結合coredump進行Native Crash分析。
6.2 debuggerd
Android手機中有個debuggerd程序,當發生Native Crash,系統會自動調用debuggerd來講資訊dump到tombstone檔案中。另外也可以主動執行debuggerd,前提是手機要root(可能還需要關閉selinux)。
debuggerd可以直接列印native調用棧,用法是
debuggerd [-b] PID
,如下:
6.3 其他工具
對于應用開發者,通常app到使用者手機上安裝之後,出現問題很難擷取對應日志,那麼使用Bugly或者google breakpad就可以拿到一些有用的日志了,原理就是前面講的信号捕獲機制。不過還是不建議在日常調試過程中啟用這類插件,避免丢掉有效的資訊。
由于常見的Native Crash問題大多是記憶體問題導緻,如果是系統開發者,還可以使用以下valgrind、checkjni和Address Sanitizer等工具進行代碼前期的問題掃描。
如果是因為加載so或者link so導緻的問題,本人實作了幾個腳本,可以友善地擷取到so檔案之間的依賴關系(便于确定加載so的順序),以及從大量的so中查找特定符号或者Java 類名。
so依賴關系通過arm-linux-androideabi-readelf及arm-linux-androideabi-nm分析so檔案資訊,再通過graphviz+dot繪制依賴圖,如下:
如果是so檔案的UnsatisfiedLinkError,那麼隻需要搜一下so中的符号就可以,比如:
7. 總結
Android上的Native Crash總的來說還是有章可循,通過分析有效的日志和調用棧以及使用正确的工具進行調試,也可以達到和Java Crash差不多的分析效率。這裡僅僅粗略地介紹了一些技術,真正掌握Native Crash的分析方法,還有很多細節可以深究。
8. 參考文獻:
[1] https://source.android.com/devices/tech/debug/native-crash
[2] ARM Stack Uwinding
[3] Stack frame unwinding on ARM
[4] https://developer.android.com/training/articles/perf-jni
[5] https://www.jianshu.com/p/295ebf42b05b
[6] http://gityuan.com/2016/06/25/android-native-crash/
[7] https://mp.weixin.qq.com/s/g-WzYF3wWAljok1XjPoo7w
[8] https://wladimir-tm4pda.github.io/porting/debugging_gdb.html
[9] https://source.android.com/devices/tech/debug/gdb
[10] https://blog.csdn.net/ly890700/article/details/53104773
文章後記:
天天P圖是由騰訊公司開發的業内領先的圖像處理,相機美拍的APP。歡迎掃碼或搜尋關注我們的微信公衆号:“天天P圖攻城獅”,那上面将陸續公開分享我們的技術實踐,期待一起交流學習!
加入我們:
天天P圖技術團隊長期招聘:
(1) AND / iOS 開發工程師 (2) 圖像處理算法工程師
期待對我們感興趣或者有推薦的技術牛人加入我們(base 上海)!聯系方式:[email protected]