日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

『网易实习』周记(五)

發布時間:2023/12/9 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 『网易实习』周记(五) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

『網易實習』周記(五)

文章目錄

  • 『網易實習』周記(五)
    • Crash監控
      • Crash的簡單定義
      • Java Crash
      • Native Crash
        • Native Crash簡介
        • so組成
        • Native Crash的發生
        • Native Crash捕獲與解析

本周知識清單:

1.調研了解native crash的收集方式
2.整理appdump中的native crash及定位

Crash監控

Crash的簡單定義

Crash(應用崩潰)是由于代碼異常而導致 App 非正常退出,導致應用程序無法繼續使用,所有工作都 停止的現象。發生 Crash 后需要重新啟動應用(有些情況會自動重啟),而且不管應用在開發階段做得 多么優秀,也無法避免 Crash 發生,在 Android 應用中發生的 Crash 有兩種類型,Java 層的 Crash 和 Native 層 Crash。這兩種Crash 的監控和獲取堆棧信息有所不同。

Java Crash

Java的Crash監控非常簡單,Java中的Thread定義了一個接口: UncaughtExceptionHandler ;用于 處理未捕獲的異常導致線程的終止(注意:被catch的異常是捕獲不到的),當我們的應用crash的時候,就會走 UncaughtExceptionHandler.uncaughtException ,在該方法中可以獲取到異常的信息,我們通 過 Thread.setDefaultUncaughtExceptionHandler 該方法來設置線程的默認異常處理器,我們可以 將異常信息保存到本地然后上傳到服務器,方便我們快速的定位問題。

public class CrashHandler implements Thread.UncaughtExceptionHandler {private static final String FILE_NAME_SUFFIX = ".trace";private static Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;private static Context context;public static void init(Context applicationContext) {context = applicationContext;defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();Thread.setDefaultUncaughtExceptionHandler(new CrashHandler());}@Overridepublic void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {try {File file = dealException(t, e);} catch (Exception exception) {} finally {if (defaultUncaughtExceptionHandler != null) {defaultUncaughtExceptionHandler.uncaughtException(t, e);}}}private File dealException(Thread thread, Throwable throwable) throws JSONException, IOException, PackageManager.NameNotFoundException {String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//私有目錄,無需權限File f = new File(context.getExternalCacheDir().getAbsoluteFile(), "crash_info");if (!f.exists()) {f.mkdirs();}File crashFile = new File(f, time + FILE_NAME_SUFFIX);PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)));pw.println(time);pw.println("Thread: " + thread.getName());pw.println(getPhoneInfo());throwable.printStackTrace(pw); //寫入crash堆棧pw.flush();pw.close();return crashFile;}private String getPhoneInfo() throws PackageManager.NameNotFoundException {PackageManager pm = context.getPackageManager();PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);StringBuilder sb = new StringBuilder();//App版本sb.append("App Version: ");sb.append(pi.versionName);sb.append("_");sb.append(pi.versionCode + "\n");//Android版本號sb.append("OS Version: ");sb.append(Build.VERSION.RELEASE);sb.append("_");sb.append(Build.VERSION.SDK_INT + "\n");//手機制造商sb.append("Vendor: ");sb.append(Build.MANUFACTURER + "\n");//手機型號sb.append("Model: ");sb.append(Build.MODEL + "\n");//CPU架構sb.append("CPU: ");if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {sb.append(Arrays.toString(Build.SUPPORTED_ABIS));} else {sb.append(Build.CPU_ABI);}return sb.toString();} }

Native Crash

Native Crash簡介

就是C或者C++運行過程中產生的錯誤,從Android系統全局來說Crash通常分為App Crash ,Native Crash,以及Kernel Crash

  • App Crash就是Java 層面的Crash,那么往往是通過拋出未捕獲的異常所導致
  • Native Crash,就是C/C++層面的Crash,是在介于framework層和Linux層之間的一層,Native Crash發生后系統會在路徑/data/tombstones 下產生一些導致Crash 的文件 tombstone_xx ,并且Google 的NDK包里面提供了一系列的調試工具,例如:addr2line,objdump,ndk-stack
  • Kernel Crash,是由于內核崩潰往往是驅動或者硬件故障出現故障

so組成

JNI是Java Native Interface的縮寫,它的主要作用是提供了若干API來實現Java和其他語言的通信(主要是C和C++)。 NDK是一系列工具的集合,它可以幫助開發者快速開發C(或者C++)的動態庫(也稱So庫),并So庫和Java應用一起打包。開發Android應用時,有時候Java層的編碼不能滿足實現需求,就需要到C/C++實現后生成SO文件,再用System.loadLibrary()加載進行調用,這里成為JNI層的實現。常見的場景如:加解密算法,音視頻編解碼等。Android 開發中通常是將 Native 層代碼打包為.so格式的動態庫文件,然后供 Java 層調用,.so庫文件通常有以下三種來源:

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

推薦閱讀:簡單認識Android SO 文件

一個完整的so庫文件包含C/C++代碼和Debug信息,這些 debug 信息會記錄 .so中所有方法的對照表,就是方法名和其偏移地址的對應表,這就是**符號表,我們可以通過addr2line+so庫+偏移地址查詢得到報錯的具體信息。**這種so庫比較大。通常 release 的.so都是需要經過 strip 操作,strip 之后的.so中的 debug 信息會被剝離,整個 so 的體積也會縮小許多。這些 debug 信息尤為重要,是我們分析 Native Crash 問題的關鍵信息,那么我們在編譯 .so 時 候務必保留一份未被 strip 的.so或者剝離后的符號表信息,以供后面問題分析。
Mac下可以使用**file**命令查看

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 // 這個是沒有被裁減

獲取 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的發生

與Java平臺不同,C++沒有通用的異常處理接口,在C層,CPU通過異常中斷的方式,觸發異常處理流程,不同的處理器,有不同的異常中斷類型和中斷處理方式,linux把這些中斷處理,統稱為信號量機制,每一種異常都有一個對應的信號,可以注冊回調函數處理需要關注的的信號量,所有信號量都是定義在<signal.h> 文件中。如下:

#define SIGHUP 1 // 終端連接結束時發出(不管正常或非正常) #define SIGINT 2 // 程序終止(例如Ctrl-C) #define SIGQUIT 3 // 程序退出(Ctrl-\) #define SIGILL 4 // 執行了非法指令,或者試圖執行數據段,堆棧溢出 #define SIGTRAP 5 // 斷點時產生,由debugger使用 #define SIGABRT 6 // 調用abort函數生成的信號,表示程序異常 #define SIGIOT 6 // 同上,更全,IO異常也會發出 #define SIGBUS 7 // 非法地址,包括內存地址對齊出錯,比如訪問一個4字節的整數, 但其地址不是4的倍數 #define SIGFPE 8 // 計算錯誤,比如除0、溢出 #define SIGKILL 9 // 強制結束程序,具有最高優先級,本信號不能被阻塞、處理和忽略 #define SIGUSR1 10 // 未使用,保留 #define SIGSEGV 11 // 非法內存操作,與SIGBUS不同,他是對合法地址的非法訪問,比如訪問沒有讀權限的內存,向沒有寫權限的地址寫數據 #define SIGUSR2 12 // 未使用,保留 #define SIGPIPE 13 // 管道破裂,通常在進程間通信產生 #define SIGALRM 14 // 定時信號, #define SIGTERM 15 // 結束程序,類似溫和的SIGKILL,可被阻塞和處理。通常程序如果終止不了,才會嘗試SIGKILL #define SIGSTKFLT 16 // 協處理器堆棧錯誤 #define SIGCHLD 17 // 子進程結束時, 父進程會收到這個信號。 #define SIGCONT 18 // 讓一個停止的進程繼續執行 #define SIGSTOP 19 // 停止進程,本信號不能被阻塞,處理或忽略 #define SIGTSTP 20 // 停止進程,但該信號可以被處理和忽略 #define SIGTTIN 21 // 當后臺作業要從用戶終端讀數據時, 該作業中的所有進程會收到SIGTTIN信號 #define SIGTTOU 22 // 類似于SIGTTIN, 但在寫終端時收到 #define SIGURG 23 // 有緊急數據或out-of-band數據到達socket時產生 #define SIGXCPU 24 // 超過CPU時間資源限制時發出 #define SIGXFSZ 25 // 當進程企圖擴大文件以至于超過文件大小資源限制 #define SIGVTALRM 26 // 虛擬時鐘信號. 類似于SIGALRM, 但是計算的是該進程占用的CPU時間. #define SIGPROF 27 // 類似于SIGALRM/SIGVTALRM, 但包括該進程用的CPU時間以及系統調用的時間 #define SIGWINCH 28 // 窗口大小改變時發出 #define SIGIO 29 // 文件描述符準備就緒, 可以開始進行輸入/輸出操作 #define SIGPOLL SIGIO // 同上,別稱 #define SIGPWR 30 // 電源異常 #define SIGSYS 31 // 非法的系統調用

常見的信號:

1·處理信號:sigaction
sigaction() 系統調用用于更改進程在接收到特定信號時所采取的操作。通過 sigaction 系統調用設置信號處理函數。如果沒有為一個信號設置對應的處理函數,就會使用默認的處理函數,否則信號就被進程截獲并調用相應的處理函數。

extern int sigaction(int, const struct sigaction*, struct sigaction*); ---- 1/第一個參數,int類型,代表要關注的信號量 2/第二個參數,sigaction結構體指針,代表當某個信號發生時,應該如何處理 3/第三個參數,也是sigaction結構體指針,它代表默認的處理方式,當我們定義了信號處理的時候, 用它之前的默認處理方式

所以要訂閱信號,最簡單的做法直接用一個循環遍歷訂閱所有要訂閱的信號,對每一個信號調用sigaction()

void init() {struct sigaction handler;struct sigaction old_signal_handlers[SIGNALS_LEN];for (int i = 0; i < SIGNALS_LEN; ++i) {sigaction(signal_array[i], &handler, & old_signal_handlers[i]);} }

2·捕捉Carsh的位置
sigaction 結構體有一個 sa_sigaction變量,他是個函數指針,原型為:void (*)(int siginfo_t *, void *)
因此,我們可以聲明一個函數,直接將函數的地址賦值給sa_sigaction

void signal_handle(int code, siginfo_t *si, void *context) {發生 Crash 的時候就會回調我們傳入的signal_handle()函數了。在signal_handle()函數中,我們得要想辦法拿到當前執行的代碼信息。 }void init() {struct sigaction old_signal_handlers[SIGNALS_LEN];struct sigaction handler;handler.sa_sigaction = signal_handle;handler.sa_flags = SA_SIGINFO;for (int i = 0; i < SIGNALS_LEN; ++i) {sigaction(signal_array[i], &handler, & old_signal_handlers[i]);} }

3·設置緊急棧空間
這種情況主要考慮的是無限遞歸造成堆棧溢出,如果在一個已溢出的堆棧處理信號,那么肯定是失敗的,可以使用sigaltstack() 在任意線程注冊一個可選的棧,系統會在危險時機把棧指針指向這個地方,使得可以在一個新的棧上運行信號處理函數

void signal_handle(int sig) {write(2, "stack overflow\n", 15);_exit(1); } unsigned infinite_recursion(unsigned x) {return infinite_recursion(x)+1; } int main() {static char stack[SIGSTKSZ];stack_t ss = {.ss_size = SIGSTKSZ,.ss_sp = stack,};struct sigaction sa = {.sa_handler = signal_handle,.sa_flags = SA_ONSTACK};sigaltstack(&ss, 0);sigfillset(&sa.sa_mask);sigaction(SIGSEGV, &sa, 0);infinite_recursion(0); }

4·獲取Crash數據
異常處理函數的第3個參數 void* context 將會用與 crash 數據的收集。context 參數是指向 ucontext_t 類型的一個指針。ucontext_t結構體會包含出現異常的線程上下文信息:

  • 執行棧
  • 存儲的寄存器
  • 阻塞的信號列表

具體的字段信息:

  • uc_link: 當前方法返回時應該返回到的地址(如果 uc_link 等于NULL,那么當這個方法返回時進程就會退出)
  • uc_sigmask: 阻塞的信號
  • uc_stack: 執行棧
  • uc_mcontext: 存儲的寄存器(uc_mcontext 字段與機器的處理器架構相關)

由于寄存器等信息在不同處理器架構下都不相同。如下是在 arm 架構下的 ucontext_t 定義:

#if defined(__arm__)#define NGREG 18 /* Like glibc. */typedef int greg_t; typedef greg_t gregset_t[NGREG]; typedef struct user_fpregs fpregset_t;#include <asm/sigcontext.h> typedef struct sigcontext mcontext_t;typedef struct ucontext {unsigned long uc_flags;struct ucontext* uc_link;stack_t uc_stack;mcontext_t uc_mcontext;sigset_t uc_sigmask;/* Android has a wrong (smaller) sigset_t on ARM. */uint32_t __padding_rt_sigset;/* The kernel adds extra padding after uc_sigmask to match glibc sigset_t on ARM. */char __padding[120];unsigned long uc_regspace[128] __attribute__((__aligned__(8))); } ucontext_t;

從上面可以看出,在 ARM 下這里會在 gregset_t 數組中儲存 18 個寄存器,而且 mcontext_t 的類型是 arm/sigcontext.h 中的 sigcontext:

struct sigcontext {unsigned long trap_no;unsigned long error_code;unsigned long oldmask;unsigned long arm_r0;unsigned long arm_r1;unsigned long arm_r2;unsigned long arm_r3;unsigned long arm_r4;unsigned long arm_r5;unsigned long arm_r6;unsigned long arm_r7;unsigned long arm_r8;unsigned long arm_r9;unsigned long arm_r10;unsigned long arm_fp;unsigned long arm_ip;unsigned long arm_sp;unsigned long arm_lr;unsigned long arm_pc;unsigned long arm_cpsr;unsigned long fault_address; };

sigcontext 中的 arm_pc 就代表了 ARM 處理器的 PC 寄存器。
5·定位問題代碼
當native代碼出現異常時,我們能夠從輸出看到問題代碼所屬文件和行數,這就涉及到so庫文件的編碼和pc程序計數器運行原理。PC寄存器存儲著處理器當前執行的指令的內存地址,獲取這個內存地址之后,使用addr2line工具就能找到你地址對應的源碼行數。
程序寄存器中存儲的當前指令在內存中的絕對地址 ,而 addr2line 工具需要的是指令在指令所屬的 so 中的相對地址,所以需要先獲取出現異常的指令屬于的共享庫(so)被加載到內存的開始地址,然后使用 絕對地址 減去 開始地址 得出程序寄存器相對 開始地址 的偏移量: 相對地址 = 絕對地址(pc) - so被加載到的地址。通過 dladdr 庫函數,可以找到一個絕對地址所屬的 so, 以及 so 被加載到內存的位置

Dl_info dl_info; LOG_D("calculate pc(%d)", absolute_pc); int result = dladdr((void *) absolute_pc, &dl_info); if (result && dl_info.dli_fname) {// so 加載到內存的地址uint base = reinterpret_cast<long>(dl_info.dli_fbase);// 當前 pc 屬于的方法的名稱LOG_D("symbol is %s", dl_info.dli_sname);// 計算相對位置uint relative_pc = absolute_pc - base;return relative_pc; } return 0;

Android Gradle Plugin 在 native 編譯時會默認對 so 進行 strip 操作,so 中與調試相關的信息都被去掉了。所以可以在 debug 編譯下禁用 strip:

android {...buildTypes {...debug {packagingOptions {doNotStrip "*/x86_64/*.so"}}}...}

示例:

  • 定義一個 NativeCatcher 空間來做native crash的收集
namespace NativeCatcher {// 只捕獲會造成進程終止的幾種異常const int SIGNALS_LEN = 7;const int signal_array[] = {SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT, SIGSYS};// 儲存系統默認的異常處理struct sigaction old_signal_handlers[SIGNALS_LEN];void init();void signal_handler(int, siginfo_t *, void *);void make_crash(); }
  • 在JNI_OnLoad的時候,調用init 設置異常處理函數
static jclass CLASS = nullptr;extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved) {NativeCatcher::init();//...return JNI_VERSION_1_4; }
  • 注冊異常處理函數, 并持有默認的處理函數。sigaction是 sigaction() 系統調用的參數, sa_flags 用于配置信號會攜帶的數據, 如果 sa_flags 含有SA_SIGINFO標志位, 則異常處理函數(sa_sigaction) 需要為 void (*sa_sigaction)(int, siginfo_t *, void *) 的函數指針,否則就需要為void (*sa_handler)(int)的函數指針。
void NativeCatcher::init() {struct sigaction handler = {.sa_sigaction = NativeCatcher::signal_handler,.sa_flags = SA_SIGINFO};for (int i = 0; i < SIGNALS_LEN; ++i) {sigaction(signal_array[i], &handler, &old_signal_handlers[i]);} }
  • 設置異常處理函數
void NativeCatcher::signal_handler(int signal, siginfo_t *info, void *context) {// 自己做一些處理工作const int code = info->si_code;LOG_D("handler signal %d, code: %d, pid: %d, uid: %d, tid: %d",signal,code,info->si_pid,info->si_uid,info->si_tid);// 找到異常對應的默認處理函數int index = -1;for (int i = 0; i < SIGNALS_LEN; ++i) {if (signal_array[i] == signal) {index = i;break;}}if (index == -1) {LOG_E("Not found match handler");exit(code);}struct sigaction old = old_signal_handlers[index];// 調用默認的異常處理函數old.sa_sigaction(signal, info, context); }
  • 模擬產生異常
void NativeCatcher::make_crash() {int a = 0;int i = 10 / a; }

推薦閱讀
Android 處理 Native Crash
Android Native Crash 收集
Android 平臺 Native Crash 捕獲原理詳解

Native Crash捕獲與解析

這里主要是下面的兩個方法:

  • 通過DropBox日志解析–適用于系統應用,Android Dropbox 是 Android 在 Froyo(API level 8) 引入的用來持續化存儲系統數據的機制。主要用于記錄 Android 運行過程中, 內核, 系統進程, 用戶進程等出現嚴重問題時的 log, 可以認為這是一個可持續存儲的系統級別的 logcat。相關文件記錄存儲目錄:/data/system/dropbox
  • 借助上述的ndk-stack工具,可以直接將DropBox下面的日志解析成堆棧
  • Android/SDK/NDK提供的工具linux-android-addr2line ,aarch64-linux-android-addr2line -f -C -e libbreakpad-core.so 00000000000161a0
  • 通過BreakPad捕獲解析–適用于所有應用,非系統應用可以通過google提供的開源工具BreakPad進行監測分析,CrashSDK也是采用的此種方式,可以實時監聽到NE的發生,并且記錄相關的文件, 從而可以將崩潰和相應的應用崩潰時的啟動、場景等結合起來上報。
  • 也是通過inux-android-addr2line解析出錯誤的方法
  • 推薦閱讀:
    Android NativeCrash 捕獲與解析
    Android 平臺 Native Crash 問題分析與定位


    總結

    以上是生活随笔為你收集整理的『网易实习』周记(五)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。