在 Android 平臺,native crash 我們可能關注得比較少,記得在長沙做開發那會,基本不會用到自己寫的 so 庫,集成第三方功能像地圖也就會拷貝幾個 so 到目錄下,當時連 so 是什麼都不知道。後來漸漸的由於項目的特殊性,不能直接集成 bugly 和 qapm 這些,因此後面就被逼着學會了 Native 層的崩潰捕獲。雖然實現起來相對要比 java 層更難一些,但也並不是很複雜,我們可以查一些資料或者借鑑一些第三方的開源庫,總結起來只需要從以下幾個方面入手即可:
- 瞭解 native 層的崩潰處理機制
- 捕捉到 native crash 信號
- 處理各種特殊情況
- 解析 native 層的 crash 堆棧
1. 瞭解 native 層的崩潰處理機制
開源庫有 coffeecatch 、 breakpad 等,普通項目中我們可以直接集成 bugly ,由於 bugly 不開源所以借鑑的意義並不大。breakpad 是 google 開源的比較權威但是代碼體積量大,coffeecatch 實現簡潔但存在兼容性問題。其實無論是 coffeecatch 還是 bugly 又或是我們自己寫,其內部的實現原理肯定都是一致的, 只要我們瞭解 native 層的崩潰處理機制,一切便能迎刃而解。
在 Unix-like 系統中,所有的崩潰都是編程錯誤或者硬件錯誤相關的,系統遇到不可恢復的錯誤時會觸發崩潰機制讓程序退出,如除零、段地址錯誤等。異常發生時,CPU 通過異常中斷的方式,觸發異常處理流程。不同的處理器,有不同的異常中斷類型和中斷處理方式。linux 把這些中斷處理,統一爲信號量,可以註冊信號量向量進行處理。信號機制是進程之間相互傳遞消息的一種方法,信號全稱爲軟中斷信號。
函數運行在用戶態,當遇到系統調用、中斷或是異常的情況時,程序會進入內核態。信號涉及到了這兩種狀態之間的轉換。
接收信號的任務是由內核代理的,當內核接收到信號後,會將其放到對應進程的信號隊列中,同時向進程發送一箇中斷,使其陷入內核態。注意,此時信號還只是在隊列中,對進程來說暫時是不知道有信號到來的。進程陷入內核態後,有兩種場景會對信號進行檢測:
- 進程從內核態返回到用戶態前進行信號檢測
- 進程在內核態中,從睡眠狀態被喚醒的時候進行信號檢測
當發現有新信號時,便會進入信號的處理。信號處理函數是運行在用戶態的,調用處理函數前,內核會將當前內核棧的內容備份拷貝到用戶棧上,並且修改指令寄存器(eip)將其指向信號處理函數。接下來進程返回到用戶態中,執行相應的信號處理函數。信號處理函數執行完成後,還需要返回內核態,檢查是否還有其它信號未處理。如果所有信號都處理完成,就會將內核棧恢復(從用戶棧的備份拷貝回來),同時恢復指令寄存器(eip)將其指向中斷前的運行位置,最後回到用戶態繼續執行進程。至此,一個完整的信號處理流程便結束了,如果同時有多個信號到達,會不斷的檢測和處理信號。
2. 捕捉到 native crash 信號
瞭解 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;
}
3. 處理各種特殊情況
Native 層的崩潰捕獲複雜就複雜在需要處理各種特殊情況,雖然一個函數就能監聽到崩潰信號回調,但是需要預防各種其他異常情況的出現,我們一一來看下:
3.1 設置額外棧空間
SIGSEGV 很有可能是棧溢出引起的,如果在默認的棧上運行很有可能會破壞程序運行的現場,無法獲取到正確的上下文。而且當棧滿了(太多次遞歸,棧上太多對象),系統會在同一個已經滿了的棧上調用 SIGSEGV 的信號處理函數,又再一次引起同樣的信號。我們應該開闢一塊新的空間作爲運行信號處理函數的棧。可以使用 sigaltstack 在任意線程註冊一個可選的棧,保留一下在緊急情況下使用的空間。(系統會在危險情況下把棧指針指向這個地方,使得可以在一個新的棧上運行信號處理函數)
/**
* 先創建一塊 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;
}
}
3.2 兼容其他 signal 處理
某些信號可能在之前已經被安裝過信號處理函數,而 sigaction 一個信號量只能註冊一個處理函數,這意味着我們的處理函數會覆蓋其他人的處理信號。保存舊的處理函數,在處理完我們的信號處理函數後,在重新運行老的處理函數就能完成兼容。
/* 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);
}
3.3 防止死鎖或者死循環
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");
}
4. 解析 native 層的 crash 堆棧
關於解析 native 層的 crash 堆棧解析,並不是一兩句話能說清楚的,因此我們打算單獨拿一次課來跟大家講。視頻鏈接地址無法發出來希望大家能夠諒解,因爲一粘貼視頻地址文章就會被簡書鎖定。大家感興趣的話,可以去我的 csdn 或者掘金找。