音視頻開(kāi)發(fā)之旅(59)- 捕獲收集、定位分析 Native崩潰

目錄

  1. Native崩潰有哪些類(lèi)型
  2. 如何捕獲收集Native崩潰
  3. 如何分析定位Native崩潰
  4. 資料
  5. 收獲

我們知道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ò)程如下

  1. 下載[Breakpad]源碼(https://chromium.googlesource.com/breakpad/breakpad/+/master)
  2. 下載配置depot_tools
  3. Breakpad依賴(lài)LSS,下載它(https://github.com/adelshokhy112/linux-syscall-support)并把 LSS 中的 linux_syscall_support.h 文件放至breakpad/src/third_party/lss/ 目錄下;
  4. 編譯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í)吧

四、資料

  1. 崩潰優(yōu)化(上):關(guān)于“崩潰”那些事兒
  2. Android 平臺(tái) Native 代碼的崩潰捕獲機(jī)制及實(shí)現(xiàn)
  3. 學(xué)會(huì)這個(gè)絕招,讓 C++ 崩潰無(wú)處可逃!
  4. Android使用Google Breakpad進(jìn)行崩潰日志管理
  5. Android NDK&JNI開(kāi)發(fā)之Native崩潰日志分析方法
  6. 異常處理 - Native 層的崩潰捕獲機(jī)制及實(shí)現(xiàn)
  7. Android NDK Tombstone/Crash 分析
  8. 安卓Native崩潰定位
  9. Android NDK墓碑/崩潰分析
  10. 如何分析、定位Android Native Crash
  11. 干貨|安卓APP崩潰捕獲方案——xCrash
    對(duì)應(yīng)的開(kāi)源項(xiàng)目—》[https://github.com/iqiyi/xCrash]
  12. Bugly-Android 平臺(tái) Native 代碼的崩潰捕獲機(jī)制及實(shí)現(xiàn)
  13. 刀鋒鐵騎:常見(jiàn)Android Native崩潰及錯(cuò)誤原因

五、收獲

通過(guò)本篇的學(xué)習(xí),了解熟悉了如何進(jìn)行native崩潰的捕獲和分析??偨Y(jié)如下:

  1. 學(xué)習(xí)實(shí)踐了通過(guò)breakpad進(jìn)行native崩潰的捕獲收集
  2. 實(shí)踐了minidump_stackwalk 把breakpad生成的dump文件轉(zhuǎn)為native崩潰信息文件,然后結(jié)合使用add2line和帶符號(hào)表的對(duì)應(yīng)的so,解析出崩潰的類(lèi)以及對(duì)應(yīng)的行數(shù)
  3. 實(shí)踐了墓碑文件的獲取以及結(jié)合ndk_stack進(jìn)行natvie崩潰堆棧解析
  4. 實(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)。

歡迎交流

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容