Android 平台 Native Crash 問題分析與定位

android攻城獅獅獅 發佈 2022-07-30T09:06:20.624492+00:00

Native Crash 是發生在 Android 系統中 C/C++ 層面的 Crash,具體可參考: # Android 平台 Native Crash 捕獲原理詳解。

一 Native Crash 簡介

Native Crash 是發生在 Android 系統中 C/C++ 層面的 Crash,具體可參考: # Android 平台 Native Crash 捕獲原理詳解

二 Native C/C++ Libraries 簡介

Android 開發中通常是將 Native 層代碼打包為.so格式的動態庫文件,然後供 java 層調用,.so庫文件通常有以下三種來源:

  • Android 系統自帶的核心組件和服務,如多媒體庫、OpenGL ES 圖形庫等
  • 引入的第三方庫
  • 開發者自行編譯生成的動態庫

2.1.so文件組成

一個完整的 .so 文件由 C/C++代碼和一些 debug 信息組成,這些 debug 信息會記錄 .so中所有方法的對照表,就是方法名和其偏移地址的對應表,也叫做符號表 symbolic 信息,這種 .so被稱為未 strip 的,通常體積會比較大。

通常 release 的.so都是需要經過 strip 操作,strip 之後的.so中的 debug 信息會被剝離,整個 so 的體積也會縮小許多。

可以簡單將這個 debug 信息理解為 Java 代碼混淆中的 mapping 文件,只有擁有這個 mapping 文件才能進行堆棧分析。如果堆棧信息丟了,基本上堆棧無法還原,問題也無法解決。

所以,這些 debug 信息尤為重要,是我們分析 Native Crash 問題的關鍵信息,那麼我們在編譯 .so 時 候務必保留一份未被 strip 的.so或者剝離後的符號表信息,以供後面問題分析。

2.2 查看 so 狀態

也可以通過命令行來查看.so的狀態,Linux 下使用 file 命令即可,在命令返回值裡面可以查看到.so的一 些基本信息。

如下代碼所示,stripped 代表是沒有 debug 信息的.so,with debug_info, not stripped 代表攜帶 debug 信息的.so

file libbreakpad-core-s.so
libbreakpad-core-s.so: *******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, stripped
file libbreakpad-core.so
libbreakpad-core.so: ******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, with debug_info, not stripped

2.3 獲取 strip 和未被 strip 的 so

目前 Android Studio 無論是使用 mk 或者 CMake 編譯的方式都會同時輸出 strip 和未 strip 的 so,如下圖是 Cmake 編譯 so 產生的兩個對應的 so。

strip 之前的 so 路徑:{project}/app/build/intermediates/merged_native_libs

strip 之後的 so 路徑:{project}/app/build/intermediates/stripped_native_libs

三 Native Crash 捕獲與解析

3.1 通過 DropBox 日誌解析

Android Dropbox 是 Android 在 Froyo(API level 8) 引入的用來持續化存儲系統數據的機制。主要用於記錄 Android 運行過程中, 內核, 系統進程, 用戶進程等出現嚴重問題時的 log, 可以認為這是一個可持續存儲的系統級別的 logcat。

相關文件記錄存儲目錄:/data/system/dropbox

只需要將 DropBox 的日誌獲取到即可進行分析解決,下面貼上一份 Log 示例。

DropBox 中的 Tombstone 文件顯示,Native Crash 發生在動態庫 libnativedemo.so 中,具體的方法和行數可以用 Android/SDK/NDK 提供的工具 linux-android-addr2line 來進一步定位。

addr2line 工具通常在 ndk 目錄下,例如:

${SDK Path}/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line

然後使用命令行,既可將偏移地址轉換為 crash 方法和行數

arm-linux-androideabi-addr2line [option(s)] [addr(s)]

簡單來說就是 arm-linux-androideabi-addr2line + 可選項 + 異常地址

[option(s)]

介紹

@

從文件中讀取 options

-a

在結果中顯示地址 addr

-b

設置二進位文件的格式

-e

設置輸入文件(常用:選項後面需要跟報錯的共享庫,用於 addr2line 程序分析)

-i

unwind inline function

-j

Read section-relative offsets instead of addresses

-p

讓輸出更易讀

-s

在輸出中,剝離文件夾名稱

-f

顯示函數名稱

-C

(大寫的) 將輸出的函數名 demangle

-h

輸出幫助

-v

輸出版本信息

使用 addr2line 進行解析,結果可以看到,Native Crash 發生在文件 native-lib.cpp17 行的 Crash() 方法

結合代碼分析,在 Crash() 中,對空指針 *a 進行了賦值操作,所以造成了 crash。

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

/**
 * 引起 crash
 */
void Crash() {
    volatile int *a = (int *) (NULL);
    *a = 1;
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
    Crash();
}

通過讀取 DropBox 獲得 crash log -> addr2line 解析偏移地址的方法確實可以定位到 native crash 發生的現場,但是 DropBox 只有系統應用能訪問,非系統應用拿不到日誌。對於非系統應用,可以使用 Google 提供的開源工具 BreakPad 進行監測分析。

3.2 通過 BreakPad 捕獲解析

3.2.1 breakpad 簡介

BreakPad 是 Google 開發的一個跨平台 C/C++ dump捕獲開源庫,崩潰文件使用微軟的 minidump格式存儲,也支持發送這個 dump 文件到你的伺服器,breakpad 可以在程序崩潰時觸發 dump 寫入操作,也可以在沒有觸發 dump 時主動寫 dump 文件。breakpad 支持 windows、linux、macos、android、ios 等。目前已有 Google Chrome, Firefox, Google Picasa, Camino, Google Earth 等項目使用。

3.2.2 實現原理

在不同平台下使用平台特有的函數以及方式實現異常捕獲:

Windows:通過 SetUnhandledexceptionFilter()設置崩潰回掉函數

Max OS:監聽 Mach Exception Port 獲取崩潰事件

Linux:監聽 SIGILL SIGSEGV 等異常信號 獲取崩潰事件

工作原理示意圖

圖片右上角是一個完整的應用程式,它包含了三部分即程序代碼、Breakpad Client(即 brekapad 提供出來的靜態庫),調式信息

  • Build System中 breakpad 的 symbol 生成工具藉助應用層序中的 Debugging Information 這一部分生成一個 Google 自己的符號文件,最終在發布應用層序的時候使用 strip 將調式信息去除
  • User's System中運行的應用程式是通過 strip 去除了調式信息的,若應用程式發生 Crash,breakpad client 就會寫 minidump 文件到指定目錄,也可以將產生的 minidump 文件發送到遠端伺服器即 Crash Colletcor。
  • Crash Collector就可以利用 Build System 中產生的 symol 文件和 User's System 中上報的 minidump 文件生成用戶可讀的 stack trace

3.2.3 使用示例

獲取 breakpad 源碼

github.com/google/brea…

執行安裝 breakpad

1. cd breakpad 目錄
2. 直接命令窗口輸入:

./configure && make

移植 Breakpad 到客戶端程序

breakpad 源碼導入應用程式 cpp 目錄下

然後在 breakpad 中創建 CMakeLists.txt

cmake_minimum_required(VERSION 3.18.1)

#導入頭文件
include_directories(src src/common/android/include)
#支持彙編文件的編譯
enable_language(ASM)
#源文件編譯為靜態庫
add_library(breakpad static
        src/client/linux/crash_generation/crash_generation_client.cc
        src/client/linux/dump_writer_common/thread_info.cc
        src/client/linux/dump_writer_common/ucontext_reader.cc
        src/client/linux/handler/Exception_handler.cc
        src/client/linux/handler/minidump_descriptor.cc
        src/client/linux/log/log.cc
        src/client/linux/microdump_writer/microdump_writer.cc
        src/client/linux/minidump_writer/linux_dumper.cc
        src/client/linux/minidump_writer/linux_ptrace_dumper.cc
        src/client/linux/minidump_writer/minidump_writer.cc
        src/client/linux/minidump_writer/pe_file.cc
        src/client/minidump_file_writer.cc
        src/common/convert_UTF.cc
        src/common/md5.cc
        src/common/string_conversion.cc
        src/common/linux/breakpad_getcontext.S
        src/common/linux/elfutils.cc
        src/common/linux/file_id.cc
        src/common/linux/guid_creator.cc
        src/common/linux/linux_libc_support.cc
        src/common/linux/memory_mapped_file.cc
        src/common/linux/safe_readlink.cc)
#導入相關的庫
target_link_libraries(breakpad log)

breakpad 中的 CMakeLists.txt 創建完成後,還需要在 cpp 目錄下的 CMakeLists.txt 中進行配置,將剛剛創建的 CMakeLists.txt 引入進去

cmake_minimum_required(VERSION 3.18.1)

#引入頭文件
include_directories(breakpad/src breakpad/src/common/android/include)

add_library(nativecrash SHARED nativecrashlib.cpp)

#添加子目錄,會自動查找這個目錄下的 CMakeList
add_subdirectory(breakpad)

target_link_libraries(nativecrash log breakpad)

breakpad 初始化

然後在自己項目的 native 文件中對 breakpad 進行初始化,如下

#include <jni.h>
#include <string>
#include "breakpad/src/client/linux/handler/exception_handler.h"
#include "breakpad/src/client/linux/handler/minidump_descriptor.h"

/**
 * 引起 crash
 */
void Crash() {
    volatile int *a = (int *) (NULL);
    *a = 1;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
    Crash();
}

//回調函數
bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
                  void* context,
                  bool succeeded) {
    printf("Dump path: %s\n", descriptor.path());
    return false;
}

//breakpad 初始化
extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_initNative(JNIEnv *env, jclass clazz, 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);
}

Java 層代碼

Java 層傳入 Crash dump 文件的保存路徑,用於崩潰時文件的生成

package com.elijah.nativedemo;

import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.io.File;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("nativedemo");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init(this);
        findViewById(R.id.crash)
                .setOnClickListener(
                        new View.OnClickListener() {
                            @Override
                            public void onClick(View view) {
                                nativeCrash();
                            }
                        });
    }

    public static void init(Context context){
        Context applicationContext = context.getApplicationContext();
        File file = new File(applicationContext.getExternalCacheDir(),"native_crash");
        if(!file.exists()){
            file.mkdirs();
        }
        initNative(file.getAbsolutePath());
    }

    /**
     * 模擬崩潰
     */
    public static native void nativeCrash();

    /**
     * 初始化 breakpad
     * @param path
     */
    private static native void initNative(String path);
}

捕獲 Crash,解析 dump

Native Crash 產生後,breakpad 會捕獲 crash 信息,生成後綴為.dmp的 dump 文件到指定目錄下。

.dmp 格式的文件通常無法查看,需要解析工具對這個文件進行解析。解析工具在步驟「執行安裝 breakpad」中就已經生成在 breakpad/src/processor目錄下,名為 minidump_stackwalk

輸入如下指令即可解析 dump 文件

./minidump_stackwalk my.dump > crash.txt

生成的 crash.txt 如下圖所示,關鍵代碼是紅框的部分,Thread 0 後面有一個 crashed 標識,說明這裡是發生崩潰的線程,而下面就是崩潰的文件以及內存地址,使用 3.1 中介紹的 addr2line 工具進行解析即可得到問題方法與行號

作者:話嘮扇貝
連結:https://juejin.cn/post/7124689738811834382
來源:稀土掘金

關鍵字: