目錄
- Native崩潰有哪些類型
- 如何捕獲收集Native崩潰
- 如何分析定位Native崩潰
- 資料
- 收獲
我們知道Java崩潰是在Java代碼中出現了未捕獲異常,導致程序異常退出,常見的異常有:NPE、OOM、ArrayIndexOutOfBoundsException、IllegalStateException、ConcurrentModificationException等等。
還有一類崩潰,也是我們不得不關注,那就是Native層崩潰,這類崩潰不像Java層崩潰那樣比較清晰的看出堆棧信息以及具體的崩潰。每當遇到是都要查找分析,寫這篇的目的是幫助自己做下記錄,也希望能幫到有類似困擾的你,下面我們開始一起學習實踐吧。
本文學習實踐的demo以張紹文《Android開發高手課》中的例子進行。
一、 Native崩潰有哪些類型
先來造一個Native崩潰,來看下Native的崩潰信息
圖片來自: 刀鋒鐵騎:常見Android Native崩潰及錯誤原因
我們可以看到有三個相關信息
Signal xx: 代表錯誤類型,我們可以先從錯誤類型上初步判斷是哪種類型的崩潰,常見的Native崩潰如下。其中 SIGSEGV時遇到的機率基本上最高的。
接下來是寄存器快照,這個直接看不出來問題,而fault addr是比較關鍵的一個信息,我們后續再分析定位時會用到它。
再接下來時調用堆棧,這個也非常重要,可以直接幫助我們看出Crash的堆棧信息,但是需要有符號表的so才能轉為對應的函數名和行數,否則也是比較難看懂。
二、如何捕獲收集Native崩潰
常見的Native崩潰捕獲工具:Chromium的BreakPad、騰訊的bugly
我們來通過學習實踐Breakpad來進行收集Natvie崩潰。Breakpad是一個跨平臺的開源項目,這一小節我們來學習實踐下如何編譯使用.
2.1 我們先來看下Breakpad的工作原理
圖片來自: 學會這個絕招,讓 C++ 崩潰無處可逃!
2.2 編譯安裝過程如下
- 下載[Breakpad]源碼(https://chromium.googlesource.com/breakpad/breakpad/+/master)
- 下載配置depot_tools
- Breakpad依賴LSS,下載它(https://github.com/adelshokhy112/linux-syscall-support)并把 LSS 中的 linux_syscall_support.h 文件放至breakpad/src/third_party/lss/ 目錄下;
- 編譯Breakpad ./configure && make && sudo make install
編譯安裝成功后可以看到生成的生成的/usr/local/bin/minidump_dump和/usr/local/bin/minidump_stackwalk工具,這些命令工具我們在后面定位分析時會用到
2.3 將Breakpad集成到Android項目中
將 google-breakpad 源代碼里面的src文件夾拷貝到項目的src/main/cpp目錄下;
配置cmake或者makefile,這里我們使用cmake
cmake_minimum_required(VERSION 3.4.1)
#設置breakpad根路徑
set(BREAKPAD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
#設置頭文件的路徑
include_directories(${BREAKPAD_ROOT}/src ${BREAKPAD_ROOT}/src/common/android/include)
#歸類要編譯的cpp代碼的文件
file(GLOB BREAKPAD_SOURCES_COMMON
${BREAKPAD_ROOT}/src/client/linux/crash_generation/crash_generation_client.cc
${BREAKPAD_ROOT}/src/client/linux/dump_writer_common/thread_info.cc
${BREAKPAD_ROOT}/src/client/linux/dump_writer_common/ucontext_reader.cc
${BREAKPAD_ROOT}/src/client/linux/handler/exception_handler.cc
${BREAKPAD_ROOT}/src/client/linux/handler/minidump_descriptor.cc
${BREAKPAD_ROOT}/src/client/linux/log/log.cc
${BREAKPAD_ROOT}/src/client/linux/microdump_writer/microdump_writer.cc
${BREAKPAD_ROOT}/src/client/linux/minidump_writer/linux_dumper.cc
${BREAKPAD_ROOT}/src/client/linux/minidump_writer/linux_ptrace_dumper.cc
${BREAKPAD_ROOT}/src/client/linux/minidump_writer/minidump_writer.cc
${BREAKPAD_ROOT}/src/client/minidump_file_writer.cc
${BREAKPAD_ROOT}/src/common/convert_UTF.c
${BREAKPAD_ROOT}/src/common/md5.cc
${BREAKPAD_ROOT}/src/common/string_conversion.cc
${BREAKPAD_ROOT}/src/common/linux/elfutils.cc
${BREAKPAD_ROOT}/src/common/linux/file_id.cc
${BREAKPAD_ROOT}/src/common/linux/guid_creator.cc
${BREAKPAD_ROOT}/src/common/linux/linux_libc_support.cc
${BREAKPAD_ROOT}/src/common/linux/memory_mapped_file.cc
${BREAKPAD_ROOT}/src/common/linux/safe_readlink.cc
)
#歸類要編譯的匯編文件
file(GLOB BREAKPAD_ASM_SOURCE ${BREAKPAD_ROOT}/src/common/android/breakpad_getcontext.S
)
set_source_files_properties(${BREAKPAD_ASM_SOURCE} PROPERTIES LANGUAGE C)
#設置生成靜態庫所需編譯的文件
add_library(breakpad STATIC ${BREAKPAD_SOURCES_COMMON} ${BREAKPAD_ASM_SOURCE})
#鏈接
target_link_libraries(breakpad log)
2.4 添加breakpad的回調
java層的未捕獲的異??梢酝ㄟ^UncaughtExceptionHandler 處理,那么使用Breakpad如何捕獲Native層的異常吶?
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback, NULL, true, -1);
具體如下:
#include <stdio.h>
#include <jni.h>
#include "client/linux/handler/exception_handler.h"
#include "client/linux/handler/minidump_descriptor.h"
void onNativeCrash(const char* info) {
JNIEnv *env = 0;
jclass crashPinClass = env->FindClass(
"com/test/crash/TestCrash");
if (crashPinClass == NULL){
return;
}
jmethodID crashReportMethod = env->GetStaticMethodID(crashPinClass,
"onNativeCrash", "(Ljava/lang/String;)V");
if (crashReportMethod == NULL) {
return;
}
jstring crashInfo = env->NewStringUTF(info);
env->CallStaticVoidMethod(crashPinClass, crashReportMethod, crashInfo);
}
//崩潰回調
bool DumpCallback(const google_breakpad::MinidumpDescriptor &descriptor,
void *context,
bool succeeded) {
onNativeCrash("");
return succeeded;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_sample_breakpad_BreakpadInit_initBreakpadNative(JNIEnv *env, jclass type, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback, NULL, true, -1);
env->ReleaseStringUTFChars(path_, path);
}
然后加載so設置崩潰后生成的dmp文件的存儲路徑即可。
收集到了崩潰,我們該如何分析吶?下面小節我們繼續學習實踐。
三、如何分析定位Native崩潰
在講解幾種常用的分析工具之前,我們先來了解下編譯生成帶符號表的so和不帶符號表的so的區別。
我們可以通過file命令來查看他們之間的區別
file cmake/debug/obj/arm64-v8a/libcrash-lib.so
ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=c776759b526457b5777fc8833f7fc0fcc46055cc, with debug_info, not stripped -->沒有剝去debug信息,即帶符號表
file transforms/stripDebugSymbol/debug/0/lib/arm64-v8a/libcrash-lib.so
ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=c776759b526457b5777fc8833f7fc0fcc46055cc, stripped -->剝去debug信息,即沒符號表
如果是我們自己開發編譯的so,在發布時要把帶符號表的so進行備份或者上傳,方便分析定位native崩潰。
需要特別注意的是:不同機器打出來的so的md5是不同的,所以發版后要保存下對應的帶符號表的so(obj目錄下的不同架構的的so)
下面我們來一起學習下,常用的幾種工具
3.1 minidump_stackwalk 導出崩潰堆棧信息
就是上面一小節中我們編譯產生的命令工具。用法如下:
minidump_stackwalk fd311404-a968-4ce0-17d5fa8a-61a8fdf1.dmp libcrash-lib.so >crash.log
生成的crash.log如下
CPU: arm64 8 CPUs
GPU: UNKNOWN
Crash reason: SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed)
0 libcrash-lib.so + 0x5e0 -->出錯的地址
x0 = 0x0000007b14ac4e00 x1 = 0x0000007fedde9894
x2 = 0x0000000000000000 x3 = 0x0000007b14a56c00
x4 = 0x0000007feddeaa00 x5 = 0x0000007a7e1a7965
@
"crash.log" 2226L, 114492B
我這我們需要下個一工具繼續分析,addr2line可以把地址轉為對應的函數名和行數。
3.2 addr2line
基本用法如下
Usage: aarch64-linux-android-addr2line [option(s)] [addr(s)]
Convert addresses into line number/file name pairs.
If no addresses are specified on the command line, they will be read from stdin
The options are:
-e --exe=<executable> Set the input file name (default is a.out) 指定輸入文件
-f --functions Show function names 顯示函數名稱
而addr2line命令所在的路徑如下,可以根據崩潰信息中的設備的cpu架構來選擇對應的addr2line。
arm: $NDK_PATH/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
arm64: $NDK_PATH/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin
示例
我們看到3.1節中我們拿到的dump中的崩潰信息是 arm64 ,崩潰地址是0x5e0,下嗎我們使用add2line來進行分析下
/Users/yangbin/Library/Android/android-ndk-r16b/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -f -e /Users/yangbin/work/avwork/thirdparty/nativecrash/Chapter01/sample/build/intermediates/cmake/debug/obj/arm64-v8a/libcrash-lib.so 0x5e0
_Z5Crashv
/Users/yangbin/work/avwork/thirdparty/nativecrash/Chapter01/sample/.externalNativeBuild/cmake/debug/arm64-v8a/../../../../src/main/cpp/crash.cpp:10
可以看到輸出了對應的錯誤類和行數,再結合錯誤原因SIGSEGV即可以快速的分析出具體的原因。
3.3 將上述過程腳本化
新建一個腳本 dumptool.sh,內容如下:
用法如下:dumptool.sh ./test /tmp/fd311404-a968-4ce0-17d5fa8a-61a8fdf1.dmp crash.log
腳本來自:學會這個絕招,讓 C++ 崩潰無處可逃!
#!/bin/bash
if [ $# != 3 ] ; then
echo "USAGE: $0 TARGET_NAME DMP_NAME OUTPUT_NAME"
echo " e.g.: $0 test fd311404-a968-4ce0-17d5fa8a-61a8fdf1.dmp crash.log"
exit 1;
fi
#獲取輸入參數
target_file_name=$1
dmp_file_name=$2
output_file_name=$3
getStackTrace() {
echo "@getStackTrace: start get StackTrace"
sym_file_name=$target_file_name'.sym'
#獲取符號文件中的第一行
line1=`head -n1 $sym_file_name`
#從第一行字符串中獲取版本號
OIFS=$IFS; IFS=" "; set -- $line1; aa=$1;bb=$2;cc=$3;dd=$4; IFS=$OIFS
version_number=$dd
#創建特定的目錄結構,并將符號文件移進去
mkdir -p ./symbols/$target_file_name/$version_number
mv $sym_file_name ./symbols/$target_file_name/$version_number
#將堆棧跟蹤信息重定向到文件中
minidump_stackwalk $dmp_file_name ./symbols > $output_file_name
}
main() {
getSymbol
if [ $? == 0 ]
then
getStackTrace
fi
}
3.4 ndk-stack
ndk-stack也是非常有用的工具,它需要結合崩潰時的Tombstone(墓碑文件)進行分析。
ndk-stack用法如下
usage: ndk-stack.py [-h] -sym SYMBOL_DIR [-i INPUT]
Symbolizes Android crashes.
optional arguments:
-h, --help show this help message and exit
-sym SYMBOL_DIR, --sym SYMBOL_DIR
directory containing unstripped .so files
-i INPUT, -dump INPUT, --dump INPUT
input filename
ndk-stack -sym $PROJECT_PATH/obj/local/armeabi-v7a -dump tombstone.txt
墓碑文件的獲取可以通過 adb bugreport來進行獲取。
下面我們看下通過命令adb bugreport來拿下墓碑文件,然后結合ndk-stack分析的過程
adb bugreport .
unzip bugreport-OnePlus5T-QKQ1.191014.012-2021-11-28-14-49-22.zip
cd FS/data/tombstones
可以看到多個墓碑文件,我們拿最近的一個進行分析
ndk-stack -sym /Users/yangbin/work/avwork/thirdparty/nativecrash/Chapter01/sample/build/intermediates/cmake/debug/obj/arm64-v8a -dump tombstone_09
3.5 IDA Pro
如果沒有符號表的so怎么辦,可以嘗試使用ida這個so逆向分析工具分析定位分析,比如我們用ida打開不帶符號表的libcrash-lib.so然后通過錯誤地址來查詢問題
具體駛入如下,我們先用ida打開帶符號表的libcrash-lib.so,然后跳轉對地址為0x5e0處
我們再用不帶符號表的libcrash-lib.so,查看下
可以看到同樣也可以定位到對應的類。不過都是一些匯編語言,需要了解下。同樣通過另外一個工具objdump也可以同樣的找對應的匯編信息,進而繼續分析。
這篇基本上就到這里了,文章斷更了兩個月,這兩個月面臨崗位變更熟悉,更重要的原因是目標實現了突然放松了,其實這才是起點,通過這兩個月工作了解熟悉,音視頻涉及的知識和應用真的非常廣泛,編解碼、渲染、傳輸、協議、播放器、圖形學、AI等等。加油吧少年,下一篇開始我們進入ffmpeg源碼解析的系列。盡量做到每周至少一篇,一起學習吧
四、資料
- 崩潰優化(上):關于“崩潰”那些事兒
- Android 平臺 Native 代碼的崩潰捕獲機制及實現
- 學會這個絕招,讓 C++ 崩潰無處可逃!
- Android使用Google Breakpad進行崩潰日志管理
- Android NDK&JNI開發之Native崩潰日志分析方法
- 異常處理 - Native 層的崩潰捕獲機制及實現
- Android NDK Tombstone/Crash 分析
- 安卓Native崩潰定位
- Android NDK墓碑/崩潰分析
- 如何分析、定位Android Native Crash
-
干貨|安卓APP崩潰捕獲方案——xCrash
對應的開源項目—》[https://github.com/iqiyi/xCrash] - Bugly-Android 平臺 Native 代碼的崩潰捕獲機制及實現
- 刀鋒鐵騎:常見Android Native崩潰及錯誤原因
五、收獲
通過本篇的學習,了解熟悉了如何進行native崩潰的捕獲和分析??偨Y如下:
- 學習實踐了通過breakpad進行native崩潰的捕獲收集
- 實踐了minidump_stackwalk 把breakpad生成的dump文件轉為native崩潰信息文件,然后結合使用add2line和帶符號表的對應的so,解析出崩潰的類以及對應的行數
- 實踐了墓碑文件的獲取以及結合ndk_stack進行natvie崩潰堆棧解析
- 實踐了通過IDA pro分析無符號表的so
感謝你的閱讀
下一篇我們再次進入ffmpeg系列,結合源碼層面學習解析。歡迎關注公眾號“音視頻開發之旅”,一起學習成長。
歡迎交流