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

目錄

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

我們知道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 編譯安裝過程如下

  1. 下載[Breakpad]源碼(https://chromium.googlesource.com/breakpad/breakpad/+/master)
  2. 下載配置depot_tools
  3. Breakpad依賴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工具,這些命令工具我們在後面定位分析時會用到

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源碼解析的系列。儘量做到每週至少一篇,一起學習吧

四、資料

  1. 崩潰優化(上):關於“崩潰”那些事兒
  2. Android 平臺 Native 代碼的崩潰捕獲機制及實現
  3. 學會這個絕招,讓 C++ 崩潰無處可逃!
  4. Android使用Google Breakpad進行崩潰日誌管理
  5. Android NDK&JNI開發之Native崩潰日誌分析方法
  6. 異常處理 - Native 層的崩潰捕獲機制及實現
  7. Android NDK Tombstone/Crash 分析
  8. 安卓Native崩潰定位
  9. Android NDK墓碑/崩潰分析
  10. 如何分析、定位Android Native Crash
  11. 乾貨|安卓APP崩潰捕獲方案——xCrash
    對應的開源項目—》[https://github.com/iqiyi/xCrash]
  12. Bugly-Android 平臺 Native 代碼的崩潰捕獲機制及實現
  13. 刀鋒鐵騎:常見Android Native崩潰及錯誤原因

五、收穫

通過本篇的學習,瞭解熟悉瞭如何進行native崩潰的捕獲和分析。總結如下:

  1. 學習實踐了通過breakpad進行native崩潰的捕獲收集
  2. 實踐了minidump_stackwalk 把breakpad生成的dump文件轉爲native崩潰信息文件,然後結合使用add2line和帶符號表的對應的so,解析出崩潰的類以及對應的行數
  3. 實踐了墓碑文件的獲取以及結合ndk_stack進行natvie崩潰堆棧解析
  4. 實踐了通過IDA pro分析無符號表的so

感謝你的閱讀

下一篇我們再次進入ffmpeg系列,結合源碼層面學習解析。歡迎關注公衆號“音視頻開發之旅”,一起學習成長。

歡迎交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章