在 Android 平臺,native crash 咱們可能關注得比較少,記得在長沙作開發那會,基本不會用到本身寫的 so 庫,集成第三方功能像地圖也就會拷貝幾個 so 到目錄下,當時連 so 是什麼都不知道。後來漸漸的因爲項目的特殊性,不能直接集成 bugly 和 qapm 這些,所以後面就被逼着學會了 Native 層的崩潰捕獲。雖然實現起來相對要比 java 層更難一些,但也並非很複雜,咱們能夠查一些資料或者借鑑一些第三方的開源庫,總結起來只須要從如下幾個方面入手便可:java
開源庫有 coffeecatch 、 breakpad 等,普通項目中咱們能夠直接集成 bugly ,因爲 bugly 不開源因此借鑑的意義並不大。breakpad 是 google 開源的比較權威可是代碼體積量大,coffeecatch 實現簡潔但存在兼容性問題。其實不管是 coffeecatch 仍是 bugly 又或是咱們本身寫,其內部的實現原理確定都是一致的, 只要咱們瞭解 native 層的崩潰處理機制,一切便能迎刃而解。linux
在 Unix-like 系統中,全部的崩潰都是編程錯誤或者硬件錯誤相關的,系統遇到不可恢復的錯誤時會觸發崩潰機制讓程序退出,如除零、段地址錯誤等。異常發生時,CPU 經過異常中斷的方式,觸發異常處理流程。不一樣的處理器,有不一樣的異常中斷類型和中斷處理方式。linux 把這些中斷處理,統一爲信號量,能夠註冊信號量向量進行處理。信號機制是進程之間相互傳遞消息的一種方法,信號全稱爲軟中斷信號。git
函數運行在用戶態,當遇到系統調用、中斷或是異常的狀況時,程序會進入內核態。信號涉及到了這兩種狀態之間的轉換。github
接收信號的任務是由內核代理的,當內核接收到信號後,會將其放到對應進程的信號隊列中,同時向進程發送一箇中斷,使其陷入內核態。注意,此時信號還只是在隊列中,對進程來講暫時是不知道有信號到來的。進程陷入內核態後,有兩種場景會對信號進行檢測:編程
當發現有新信號時,便會進入信號的處理。信號處理函數是運行在用戶態的,調用處理函數前,內核會將當前內核棧的內容備份拷貝到用戶棧上,而且修改指令寄存器(eip)將其指向信號處理函數。接下來進程返回到用戶態中,執行相應的信號處理函數。信號處理函數執行完成後,還須要返回內核態,檢查是否還有其它信號未處理。若是全部信號都處理完成,就會將內核棧恢復(從用戶棧的備份拷貝回來),同時恢復指令寄存器(eip)將其指向中斷前的運行位置,最後回到用戶態繼續執行進程。至此,一個完整的信號處理流程便結束了,若是同時有多個信號到達,會不斷的檢測和處理信號。markdown
瞭解 native 層的崩潰處理機制,那麼咱們的實現方案即是註冊信號處理函數,在 native 層能夠用 sigaction():函數
#include <signal.h>
// signum:表明信號編碼,能夠是除SIGKILL及SIGSTOP外的任何一個特定有效的信號,若是爲這兩個信號定義本身的處理函數,將致使信號安裝錯誤。
// act:指向結構體sigaction的一個實例的指針,該實例指定了對特定信號的處理,若是設置爲空,進程會執行默認處理。
// oldact:和參數act相似,只不過保存的是原來對相應信號的處理,也可設置爲NULL。
// int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact));
void signal_pass(int code, siginfo_t *si, void *sc) {
LOGD("捕捉到了 native crash 信號.");
}
bool installHandlersLocked() {
if (handlers_installed)
return false;
// Fail if unable to store all the old handlers.
for (int i = 0; i < kNumHandledSignals; ++i) {
if (sigaction(kExceptionSignals[i], NULL, &old_handlers[i]) == -1) {
return false;
} else {
handlerMaps->insert(
std::pair<int, struct sigaction *>(kExceptionSignals[i], &old_handlers[i]));
}
}
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
// Mask all exception signals when we're handling one of them.
for (int i = 0; i < kNumHandledSignals; ++i)
sigaddset(&sa.sa_mask, kExceptionSignals[i]);
sa.sa_sigaction = signal_pass;
sa.sa_flags = SA_ONSTACK | SA_SIGINFO;
for (int i = 0; i < kNumHandledSignals; ++i) {
if (sigaction(kExceptionSignals[i], &sa, NULL) == -1) {
// At this point it is impractical to back out changes, and so failure to
// install a signal is intentionally ignored.
}
}
handlers_installed = true;
return true;
}
複製代碼
Native 層的崩潰捕獲複雜就複雜在須要處理各類特殊狀況,雖然一個函數就能監聽到崩潰信號回調,可是須要預防各類其餘異常狀況的出現,咱們一一來看下:oop
SIGSEGV 頗有多是棧溢出引發的,若是在默認的棧上運行頗有可能會破壞程序運行的現場,沒法獲取到正確的上下文。並且當棧滿了(太屢次遞歸,棧上太多對象),系統會在同一個已經滿了的棧上調用 SIGSEGV 的信號處理函數,又再一次引發一樣的信號。咱們應該開闢一塊新的空間做爲運行信號處理函數的棧。可使用 sigaltstack 在任意線程註冊一個可選的棧,保留一下在緊急狀況下使用的空間。(系統會在危險狀況下把棧指針指向這個地方,使得能夠在一個新的棧上運行信號處理函數)this
/**
* 先建立一塊 sigaltstack ,由於有多是由堆棧溢出發出的信號
*/
static void installAlternateStackLocked() {
if (stack_installed)
return;
memset(&old_stack, 0, sizeof(old_stack));
memset(&new_stack, 0, sizeof(new_stack));
// SIGSTKSZ may be too small to prevent the signal handlers from overrunning
// the alternative stack. Ensure that the size of the alternative stack is
// large enough.
static const unsigned kSigStackSize = std::max(16384, SIGSTKSZ);
// Only set an alternative stack if there isn't already one, or if the current
// one is too small.
if (sigaltstack(NULL, &old_stack) == -1 || !old_stack.ss_sp ||
old_stack.ss_size < kSigStackSize) {
new_stack.ss_sp = calloc(1, kSigStackSize);
new_stack.ss_size = kSigStackSize;
if (sigaltstack(&new_stack, NULL) == -1) {
free(new_stack.ss_sp);
return;
}
stack_installed = true;
}
}
複製代碼
某些信號可能在以前已經被安裝過信號處理函數,而 sigaction 一個信號量只能註冊一個處理函數,這意味着咱們的處理函數會覆蓋其餘人的處理信號。保存舊的處理函數,在處理完咱們的信號處理函數後,在從新運行老的處理函數就能完成兼容。google
/* Call the old handler. */
void call_old_signal_handler(const int sig, siginfo_t *const info, void *const sc) {
// 恢復默認應該也行吧
LOGD("sig -> %d", sig);
handlerMaps->at(sig)->sa_sigaction(sig, info, sc);
}
複製代碼
void signal_pass(int code, siginfo_t *si, void *sc) {
/* Ensure we do not deadlock. Default of ALRM is to die.
* (signal() and alarm() are signal-safe) */
// 這裏要考慮用非信號方式防止死鎖
signal(code, SIG_DFL);
signal(SIGALRM, SIG_DFL);
/* Ensure we do not deadlock. Default of ALRM is to die.
* (signal() and alarm() are signal-safe) */
(void) alarm(8);
/* Available context ? */
notifyCaughtSignal();
call_old_signal_handler(code, si, sc);
LOGD("at the end of signal_pass");
}
複製代碼
關於解析 native 層的 crash 堆棧解析,並非一兩句話能說清楚的,所以咱們打算單獨拿一次課來跟你們講。
視頻地址:pan.baidu.com/s/1FeZjyrnv… 視頻密碼:gr11