目錄
- Native崩潰有哪些類(lèi)型
- 如何捕獲收集Native崩潰
- 如何分析定位Native崩潰
- 資料
- 收獲
我們知道Java崩潰是在Java代碼中出現(xiàn)了未捕獲異常,導(dǎo)致程序異常退出,常見(jiàn)的異常有:NPE、OOM、ArrayIndexOutOfBoundsException、IllegalStateException、ConcurrentModificationException等等。
還有一類(lèi)崩潰,也是我們不得不關(guān)注,那就是Native層崩潰,這類(lèi)崩潰不像Java層崩潰那樣比較清晰的看出堆棧信息以及具體的崩潰。每當(dāng)遇到是都要查找分析,寫(xiě)這篇的目的是幫助自己做下記錄,也希望能幫到有類(lèi)似困擾的你,下面我們開(kāi)始一起學(xué)習(xí)實(shí)踐吧。
本文學(xué)習(xí)實(shí)踐的demo以張紹文《Android開(kāi)發(fā)高手課》中的例子進(jìn)行。
一、 Native崩潰有哪些類(lèi)型
先來(lái)造一個(gè)Native崩潰,來(lái)看下Native的崩潰信息

圖片來(lái)自: 刀鋒鐵騎:常見(jiàn)Android Native崩潰及錯(cuò)誤原因
我們可以看到有三個(gè)相關(guān)信息
Signal xx: 代表錯(cuò)誤類(lèi)型,我們可以先從錯(cuò)誤類(lèi)型上初步判斷是哪種類(lèi)型的崩潰,常見(jiàn)的Native崩潰如下。其中 SIGSEGV時(shí)遇到的機(jī)率基本上最高的。

接下來(lái)是寄存器快照,這個(gè)直接看不出來(lái)問(wèn)題,而fault addr是比較關(guān)鍵的一個(gè)信息,我們后續(xù)再分析定位時(shí)會(huì)用到它。
再接下來(lái)時(shí)調(diào)用堆棧,這個(gè)也非常重要,可以直接幫助我們看出Crash的堆棧信息,但是需要有符號(hào)表的so才能轉(zhuǎn)為對(duì)應(yīng)的函數(shù)名和行數(shù),否則也是比較難看懂。
二、如何捕獲收集Native崩潰
常見(jiàn)的Native崩潰捕獲工具:Chromium的BreakPad、騰訊的bugly
我們來(lái)通過(guò)學(xué)習(xí)實(shí)踐Breakpad來(lái)進(jìn)行收集Natvie崩潰。Breakpad是一個(gè)跨平臺(tái)的開(kāi)源項(xiàng)目,這一小節(jié)我們來(lái)學(xué)習(xí)實(shí)踐下如何編譯使用.
2.1 我們先來(lái)看下Breakpad的工作原理

圖片來(lái)自: 學(xué)會(huì)這個(gè)絕招,讓 C++ 崩潰無(wú)處可逃!
2.2 編譯安裝過(guò)程如下
- 下載[Breakpad]源碼(https://chromium.googlesource.com/breakpad/breakpad/+/master)
- 下載配置depot_tools
- Breakpad依賴(lài)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工具,這些命令工具我們?cè)诤竺娑ㄎ环治鰰r(shí)會(huì)用到
2.3 將Breakpad集成到Android項(xiàng)目中
將 google-breakpad 源代碼里面的src文件夾拷貝到項(xiàng)目的src/main/cpp目錄下;
配置cmake或者makefile,這里我們使用cmake
cmake_minimum_required(VERSION 3.4.1)
#設(shè)置breakpad根路徑
set(BREAKPAD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
#設(shè)置頭文件的路徑
include_directories(${BREAKPAD_ROOT}/src ${BREAKPAD_ROOT}/src/common/android/include)
#歸類(lèi)要編譯的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
)
#歸類(lèi)要編譯的匯編文件
file(GLOB BREAKPAD_ASM_SOURCE ${BREAKPAD_ROOT}/src/common/android/breakpad_getcontext.S
)
set_source_files_properties(${BREAKPAD_ASM_SOURCE} PROPERTIES LANGUAGE C)
#設(shè)置生成靜態(tài)庫(kù)所需編譯的文件
add_library(breakpad STATIC ${BREAKPAD_SOURCES_COMMON} ${BREAKPAD_ASM_SOURCE})
#鏈接
target_link_libraries(breakpad log)
2.4 添加breakpad的回調(diào)
java層的未捕獲的異??梢酝ㄟ^(guò)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);
}
//崩潰回調(diào)
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設(shè)置崩潰后生成的dmp文件的存儲(chǔ)路徑即可。
收集到了崩潰,我們?cè)撊绾畏治鰠??下面小?jié)我們繼續(xù)學(xué)習(xí)實(shí)踐。
三、如何分析定位Native崩潰
在講解幾種常用的分析工具之前,我們先來(lái)了解下編譯生成帶符號(hào)表的so和不帶符號(hào)表的so的區(qū)別。

我們可以通過(guò)file命令來(lái)查看他們之間的區(qū)別
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 -->沒(méi)有剝?nèi)ebug信息,即帶符號(hào)表
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 -->剝?nèi)ebug信息,即沒(méi)符號(hào)表
如果是我們自己開(kāi)發(fā)編譯的so,在發(fā)布時(shí)要把帶符號(hào)表的so進(jìn)行備份或者上傳,方便分析定位native崩潰。
需要特別注意的是:不同機(jī)器打出來(lái)的so的md5是不同的,所以發(fā)版后要保存下對(duì)應(yīng)的帶符號(hào)表的so(obj目錄下的不同架構(gòu)的的so)
下面我們來(lái)一起學(xué)習(xí)下,常用的幾種工具
3.1 minidump_stackwalk 導(dǎo)出崩潰堆棧信息
就是上面一小節(jié)中我們編譯產(chǎn)生的命令工具。用法如下:
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 -->出錯(cuò)的地址
x0 = 0x0000007b14ac4e00 x1 = 0x0000007fedde9894
x2 = 0x0000000000000000 x3 = 0x0000007b14a56c00
x4 = 0x0000007feddeaa00 x5 = 0x0000007a7e1a7965
@
"crash.log" 2226L, 114492B
我這我們需要下個(gè)一工具繼續(xù)分析,addr2line可以把地址轉(zhuǎn)為對(duì)應(yīng)的函數(shù)名和行數(shù)。
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 顯示函數(shù)名稱(chēng)
而addr2line命令所在的路徑如下,可以根據(jù)崩潰信息中的設(shè)備的cpu架構(gòu)來(lái)選擇對(duì)應(yīng)的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節(jié)中我們拿到的dump中的崩潰信息是 arm64 ,崩潰地址是0x5e0,下嗎我們使用add2line來(lái)進(jìn)行分析下
/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
可以看到輸出了對(duì)應(yīng)的錯(cuò)誤類(lèi)和行數(shù),再結(jié)合錯(cuò)誤原因SIGSEGV即可以快速的分析出具體的原因。
3.3 將上述過(guò)程腳本化
新建一個(gè)腳本 dumptool.sh,內(nèi)容如下:
用法如下:dumptool.sh ./test /tmp/fd311404-a968-4ce0-17d5fa8a-61a8fdf1.dmp crash.log
腳本來(lái)自:學(xué)會(huì)這個(gè)絕招,讓 C++ 崩潰無(wú)處可逃!
#!/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
#獲取輸入?yún)?shù)
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'
#獲取符號(hào)文件中的第一行
line1=`head -n1 $sym_file_name`
#從第一行字符串中獲取版本號(hào)
OIFS=$IFS; IFS=" "; set -- $line1; aa=$1;bb=$2;cc=$3;dd=$4; IFS=$OIFS
version_number=$dd
#創(chuàng)建特定的目錄結(jié)構(gòu),并將符號(hào)文件移進(jìn)去
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也是非常有用的工具,它需要結(jié)合崩潰時(shí)的Tombstone(墓碑文件)進(jìn)行分析。
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
墓碑文件的獲取可以通過(guò) adb bugreport來(lái)進(jìn)行獲取。
下面我們看下通過(guò)命令adb bugreport來(lái)拿下墓碑文件,然后結(jié)合ndk-stack分析的過(guò)程
adb bugreport .
unzip bugreport-OnePlus5T-QKQ1.191014.012-2021-11-28-14-49-22.zip
cd FS/data/tombstones
可以看到多個(gè)墓碑文件,我們拿最近的一個(gè)進(jìn)行分析
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
如果沒(méi)有符號(hào)表的so怎么辦,可以嘗試使用ida這個(gè)so逆向分析工具分析定位分析,比如我們用ida打開(kāi)不帶符號(hào)表的libcrash-lib.so然后通過(guò)錯(cuò)誤地址來(lái)查詢(xún)問(wèn)題
具體駛?cè)肴缦?,我們先用ida打開(kāi)帶符號(hào)表的libcrash-lib.so,然后跳轉(zhuǎn)對(duì)地址為0x5e0處

我們?cè)儆貌粠Х?hào)表的libcrash-lib.so,查看下

可以看到同樣也可以定位到對(duì)應(yīng)的類(lèi)。不過(guò)都是一些匯編語(yǔ)言,需要了解下。同樣通過(guò)另外一個(gè)工具objdump也可以同樣的找對(duì)應(yīng)的匯編信息,進(jìn)而繼續(xù)分析。
這篇基本上就到這里了,文章斷更了兩個(gè)月,這兩個(gè)月面臨崗位變更熟悉,更重要的原因是目標(biāo)實(shí)現(xiàn)了突然放松了,其實(shí)這才是起點(diǎn),通過(guò)這兩個(gè)月工作了解熟悉,音視頻涉及的知識(shí)和應(yīng)用真的非常廣泛,編解碼、渲染、傳輸、協(xié)議、播放器、圖形學(xué)、AI等等。加油吧少年,下一篇開(kāi)始我們進(jìn)入ffmpeg源碼解析的系列。盡量做到每周至少一篇,一起學(xué)習(xí)吧
四、資料
- 崩潰優(yōu)化(上):關(guān)于“崩潰”那些事兒
- Android 平臺(tái) Native 代碼的崩潰捕獲機(jī)制及實(shí)現(xiàn)
- 學(xué)會(huì)這個(gè)絕招,讓 C++ 崩潰無(wú)處可逃!
- Android使用Google Breakpad進(jìn)行崩潰日志管理
- Android NDK&JNI開(kāi)發(fā)之Native崩潰日志分析方法
- 異常處理 - Native 層的崩潰捕獲機(jī)制及實(shí)現(xiàn)
- Android NDK Tombstone/Crash 分析
- 安卓Native崩潰定位
- Android NDK墓碑/崩潰分析
- 如何分析、定位Android Native Crash
-
干貨|安卓APP崩潰捕獲方案——xCrash
對(duì)應(yīng)的開(kāi)源項(xiàng)目—》[https://github.com/iqiyi/xCrash] - Bugly-Android 平臺(tái) Native 代碼的崩潰捕獲機(jī)制及實(shí)現(xiàn)
- 刀鋒鐵騎:常見(jiàn)Android Native崩潰及錯(cuò)誤原因
五、收獲
通過(guò)本篇的學(xué)習(xí),了解熟悉了如何進(jìn)行native崩潰的捕獲和分析??偨Y(jié)如下:
- 學(xué)習(xí)實(shí)踐了通過(guò)breakpad進(jìn)行native崩潰的捕獲收集
- 實(shí)踐了minidump_stackwalk 把breakpad生成的dump文件轉(zhuǎn)為native崩潰信息文件,然后結(jié)合使用add2line和帶符號(hào)表的對(duì)應(yīng)的so,解析出崩潰的類(lèi)以及對(duì)應(yīng)的行數(shù)
- 實(shí)踐了墓碑文件的獲取以及結(jié)合ndk_stack進(jìn)行natvie崩潰堆棧解析
- 實(shí)踐了通過(guò)IDA pro分析無(wú)符號(hào)表的so
感謝你的閱讀
下一篇我們?cè)俅芜M(jìn)入ffmpeg系列,結(jié)合源碼層面學(xué)習(xí)解析。歡迎關(guān)注公眾號(hào)“音視頻開(kāi)發(fā)之旅”,一起學(xué)習(xí)成長(zhǎng)。
歡迎交流