轉自:http://crash.163.com/index.do#news/!newsId=2
出於執行效率、業務安全、復用已有代碼的需求,目前市場上越來越多的 Android App 采用 C/C++ 來實現其關鍵邏輯。C/C++ 有內存管理靈活、與 linux 底層聯系更緊密、多種編程范式等特點,但也正是由於這些特點,使得普通開發人員在使用 C/C++ 開發時,更容易出讓進程直接崩潰的 bug。所以能分析 C/C++ 崩潰日志並能從日志中分析出原因,成為 Android 開發人員一項必備技能。本文介紹如何通過分析 Native 崩潰日志來定位出錯的 C/C++ 代碼及出錯原因。
一、Native 崩潰日志格式
extern "C" JNIEXPORT int gen_stack(int i) { if (i > 2) return gen_stack(i - 2) + gen_stack(i - 1); else { int *p = NULL; *p = 123; return 1; } }
當調用 gen_stack(4) 發生 Native 崩潰時,一般 logcat 會打印如下格式的日志:
#00 pc 00000c1c /data/app-lib/com.testbugrpt-1/libtestNDKCrash.so (gen_stack+27)
#00 | 表示堆棧序號 |
pc 00000c1c | 表示崩潰發生時 程序計數器 位於 libtestNDKCrash.so 偏移 0xc1c 處 |
gen_stack+27 | 表示0xc1c處正好是 gen_stack 符號(此處為函數名)偏移為27的一條指令 |
#01 pc 00000c0f /data/app-lib/com.testbugrpt-1/libtestNDKCrash.so (gen_stack+14)
這是第二層堆棧,表示在離 libtestNDKCrash.so 0xc0f(也就是gen_stack + 14)位置的指令發生了一次函數調用,產生了第一層堆棧。
二、Native崩潰分析工具
在介紹工具之前,先簡單講一下有調試與無調試信息的兩個版本 so 。 一個含有 native 代碼的 app 項目的典型結構是這樣的:
--jni --Android.mk --其它源文件 --libs --armeabi --armeabi-v7a --arm64-v8a .... --obj --local --armeabi --armeabi-v7a --arm64-v8a ....
通常一次編譯會先生成一個有含有調試信息的 so, 路徑通常是在 obj/local/ 各 abi 目錄下,其中還有一些中間文件(比如.o文件);再通過對這些含有調試信息的 so 進行一次 strip , 產生對應的無調試信息 so, 放到 libs 目錄下各 abi 目錄中, 發布產品時,我們都是用這些 strip 后的 so。
一般的分析崩潰日志的工具都是利用含有調試信息的 so, 結合崩潰信息,分析崩潰點在源代碼中的行號。
- 1、ndk-stack
ndk-stack.exe位於ndk根目錄。運行以下命令:
D:\Android\android-ndk-r10c\ndk-stack.exe -sym E:\workspace\TestBugrpt\app\src\main\obj\local\armeabi-v7a\ -dump log.txt
其中 log.txt 為崩潰日志,可以從 monitor 中點擊保存得到。或者運行:
adb logcat | ndk-stack.exe -sym E:\workspace\TestBugrpt\app\src\main\obj\local\armeabi-v7a\
這樣再運行程序,當崩潰發生時,ndk-stack.exe 會自動從 logcat 中獲取崩潰日志。
運行以上命令時,要 注意 -sym 參數指示的路徑都是 obj\local\ 目錄,同時要匹配對應機器的 abi 目錄。可以得到:
表明gen_stack + 27對應testNDKCrash.cpp的第13行,即*p = 123; 查看對應的源代碼,可以發現是此處的寫空指針導致崩潰。
- 2、addr2line
addr2line 一般位於 android-ndk-r10c\toolchains\arm-linux-androideabi-4.9\prebuilt\windows\bin\ ,其路徑與文件名因操作系統、 abi 不同而有所不同。
可以運行如下命令:
arm-linux-androideabi-addr2line.exe -e E:\workspace\TestBugrpt\app\src\main\obj\local\armeabi-v7a\libtestNDKCrash.so 00000c1c 00000c0f
與 ndk-stack 不同的是,ndk-stack 接受一個 obj/local/abi 目錄為參數,而 addr2line 接受 local 下一個具體的 so 文件路徑為參數。其中 00000c1c 00000c0f 就是上面第一節中分析的崩潰點離libtestNDKCrash.so的偏移量,即
得到輸出:
E:/workspace/TestBugrpt/app/src/main//jni/testNDKCrash.cpp:13
E:/workspace/TestBugrpt/app/src/main//jni/testNDKCrash.cpp:9
分別對應兩個偏移在源碼中的位置。
- 3、objdump
上面兩種工具都是將崩潰點對應到源碼再進行分析,objdump 則是可以在匯編層對崩潰原因進行分析。當然這要求開發人員了解一些 arm/x86 匯編知識。
objdump 也是 ndk 自帶的一個工具,通常與 addr2line 在同一目錄。運行如下命令:
arm-linux-androideabi-objdump.exe -S -D E:\workspace\TestBugrpt\app\src\main\obj\local\armeabi-v7a\libtestNDKCrash.so > e:\dump.txt
由於輸出比較多,將輸出重定位到 e:\\dump.txt 便於查看。打開 dump.txt , 定位到 00000c1c :
int *p = NULL; *p = 123; c16: 2300 movs r3, #0 c18: 227b movs r2, #123 ; 0x7b c1a: 1c68 adds r0, r5, #1 c1c: 601a str r2, [r3, #0]
上面兩句是源代碼,下是對應的Arm匯編。
如果要分析的 so 沒有調試信息, ndk-stack 與 addr2line 就無能為力了,只有 objdump 還能派上用場。當然,這種情況下有更好用的工具,比如 IDA Pro。不過那又是另外一個故事了。
三、常見崩潰類型及原因
- 1、SIGSEGV 段錯誤
SEGV_MAPERR 要訪問的地址沒有映射到內存空間。 比如上面對空指針的寫操作, 當指針被意外復寫為一個較小的數值時。 SEGV_ACCERR 訪問的地址沒有權限。比如試圖對代碼段進行寫操作。 - 2、SIGFPE 浮點錯誤,一般發生在算術運行出錯時。
FPE_INTDIV 除以0 FPE_INTOVE 整數溢出 - 3、SIGBUS 總線錯誤
BUS_ADRALN 地址對齊出錯。arm cpu比x86 cpu 要求更嚴格的對齊機制,所以在 arm cpu 機器中比較常見。 - 4、SIGILL 發生這種錯誤一般是由於某處內存被意外改寫了。
ILL_ILLOPC 非法的指令操作碼 ILL_ILLOPN 非法的指令操作數 - 5、當調用堆棧中出現 stack_chk_fail 函數時,一般是由於比如 strcpy 之類的函數調用將棧上的內容覆蓋,而引起棧檢查失敗。
更多信號信息請參考文獻 [1]。
四、線上Native崩潰處理
對於第二節中的分析方法,前提是可以得到 Native 層崩潰日志。由於 Android 設備的碎片化,必然存在在測試時覆蓋不到的機型。如果 App 在用戶機器上發生了崩潰,如何獲取 Native 崩潰日志?
目前網易雲捕已經實現了對 Java、Native 層崩潰日志的獲取,並能自動上傳到服務器進行分析。具體功能及接入方法請參考網易雲捕集成說明。
參考文獻:
[1] http://man7.org/linux/man-pages/man2/sigaction.2.html
[2] http://blog.csdn.net/xyang81/article/details/42319789