『网易实习』周记(五)
『網易實習』周記(五)
文章目錄
- 『網易實習』周記(五)
- 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**命令查看:
獲取 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 系統調用設置信號處理函數。如果沒有為一個信號設置對應的處理函數,就會使用默認的處理函數,否則信號就被進程截獲并調用相應的處理函數。
所以要訂閱信號,最簡單的做法直接用一個循環遍歷訂閱所有要訂閱的信號,對每一個信號調用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
3·設置緊急棧空間
這種情況主要考慮的是無限遞歸造成堆棧溢出,如果在一個已溢出的堆棧處理信號,那么肯定是失敗的,可以使用sigaltstack() 在任意線程注冊一個可選的棧,系統會在危險時機把棧指針指向這個地方,使得可以在一個新的棧上運行信號處理函數
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 被加載到內存的位置
Android Gradle Plugin 在 native 編譯時會默認對 so 進行 strip 操作,so 中與調試相關的信息都被去掉了。所以可以在 debug 編譯下禁用 strip:
android {...buildTypes {...debug {packagingOptions {doNotStrip "*/x86_64/*.so"}}}...}示例:
- 定義一個 NativeCatcher 空間來做native crash的收集
- 在JNI_OnLoad的時候,調用init 設置異常處理函數
- 注冊異常處理函數, 并持有默認的處理函數。sigaction是 sigaction() 系統調用的參數, sa_flags 用于配置信號會攜帶的數據, 如果 sa_flags 含有SA_SIGINFO標志位, 則異常處理函數(sa_sigaction) 需要為 void (*sa_sigaction)(int, siginfo_t *, void *) 的函數指針,否則就需要為void (*sa_handler)(int)的函數指針。
- 設置異常處理函數
- 模擬產生異常
推薦閱讀:
Android 處理 Native Crash
Android Native Crash 收集
Android 平臺 Native Crash 捕獲原理詳解
Native Crash捕獲與解析
這里主要是下面的兩個方法:
推薦閱讀:
Android NativeCrash 捕獲與解析
Android 平臺 Native Crash 問題分析與定位
總結
以上是生活随笔為你收集整理的『网易实习』周记(五)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python爬虫学习二爬虫基础了解
- 下一篇: Android--Activity四种启