在 Android 平臺(tái),native crash 我們可能關(guān)注得比較少,記得在長(zhǎng)沙做開發(fā)那會(huì),基本不會(huì)用到自己寫的 so 庫(kù),集成第三方功能像地圖也就會(huì)拷貝幾個(gè) so 到目錄下,當(dāng)時(shí)連 so 是什么都不知道。后來漸漸的由于項(xiàng)目的特殊性,不能直接集成 bugly 和 qapm 這些,因此后面就被逼著學(xué)會(huì)了 Native 層的崩潰捕獲。雖然實(shí)現(xiàn)起來相對(duì)要比 java 層更難一些,但也并不是很復(fù)雜,我們可以查一些資料或者借鑒一些第三方的開源庫(kù),總結(jié)起來只需要從以下幾個(gè)方面入手即可:
- 了解 native 層的崩潰處理機(jī)制
- 捕捉到 native crash 信號(hào)
- 處理各種特殊情況
- 解析 native 層的 crash 堆棧
1. 了解 native 層的崩潰處理機(jī)制
開源庫(kù)有 coffeecatch 、 breakpad 等,普通項(xiàng)目中我們可以直接集成 bugly ,由于 bugly 不開源所以借鑒的意義并不大。breakpad 是 google 開源的比較權(quán)威但是代碼體積量大,coffeecatch 實(shí)現(xiàn)簡(jiǎn)潔但存在兼容性問題。其實(shí)無論是 coffeecatch 還是 bugly 又或是我們自己寫,其內(nèi)部的實(shí)現(xiàn)原理肯定都是一致的, 只要我們了解 native 層的崩潰處理機(jī)制,一切便能迎刃而解。
在 Unix-like 系統(tǒng)中,所有的崩潰都是編程錯(cuò)誤或者硬件錯(cuò)誤相關(guān)的,系統(tǒng)遇到不可恢復(fù)的錯(cuò)誤時(shí)會(huì)觸發(fā)崩潰機(jī)制讓程序退出,如除零、段地址錯(cuò)誤等。異常發(fā)生時(shí),CPU 通過異常中斷的方式,觸發(fā)異常處理流程。不同的處理器,有不同的異常中斷類型和中斷處理方式。linux 把這些中斷處理,統(tǒng)一為信號(hào)量,可以注冊(cè)信號(hào)量向量進(jìn)行處理。信號(hào)機(jī)制是進(jìn)程之間相互傳遞消息的一種方法,信號(hào)全稱為軟中斷信號(hào)。
函數(shù)運(yùn)行在用戶態(tài),當(dāng)遇到系統(tǒng)調(diào)用、中斷或是異常的情況時(shí),程序會(huì)進(jìn)入內(nèi)核態(tài)。信號(hào)涉及到了這兩種狀態(tài)之間的轉(zhuǎn)換。

接收信號(hào)的任務(wù)是由內(nèi)核代理的,當(dāng)內(nèi)核接收到信號(hào)后,會(huì)將其放到對(duì)應(yīng)進(jìn)程的信號(hào)隊(duì)列中,同時(shí)向進(jìn)程發(fā)送一個(gè)中斷,使其陷入內(nèi)核態(tài)。注意,此時(shí)信號(hào)還只是在隊(duì)列中,對(duì)進(jìn)程來說暫時(shí)是不知道有信號(hào)到來的。進(jìn)程陷入內(nèi)核態(tài)后,有兩種場(chǎng)景會(huì)對(duì)信號(hào)進(jìn)行檢測(cè):
- 進(jìn)程從內(nèi)核態(tài)返回到用戶態(tài)前進(jìn)行信號(hào)檢測(cè)
- 進(jìn)程在內(nèi)核態(tài)中,從睡眠狀態(tài)被喚醒的時(shí)候進(jìn)行信號(hào)檢測(cè)
當(dāng)發(fā)現(xiàn)有新信號(hào)時(shí),便會(huì)進(jìn)入信號(hào)的處理。信號(hào)處理函數(shù)是運(yùn)行在用戶態(tài)的,調(diào)用處理函數(shù)前,內(nèi)核會(huì)將當(dāng)前內(nèi)核棧的內(nèi)容備份拷貝到用戶棧上,并且修改指令寄存器(eip)將其指向信號(hào)處理函數(shù)。接下來進(jìn)程返回到用戶態(tài)中,執(zhí)行相應(yīng)的信號(hào)處理函數(shù)。信號(hào)處理函數(shù)執(zhí)行完成后,還需要返回內(nèi)核態(tài),檢查是否還有其它信號(hào)未處理。如果所有信號(hào)都處理完成,就會(huì)將內(nèi)核?;謴?fù)(從用戶棧的備份拷貝回來),同時(shí)恢復(fù)指令寄存器(eip)將其指向中斷前的運(yùn)行位置,最后回到用戶態(tài)繼續(xù)執(zhí)行進(jìn)程。至此,一個(gè)完整的信號(hào)處理流程便結(jié)束了,如果同時(shí)有多個(gè)信號(hào)到達(dá),會(huì)不斷的檢測(cè)和處理信號(hào)。
2. 捕捉到 native crash 信號(hào)
了解 native 層的崩潰處理機(jī)制,那么我們的實(shí)現(xiàn)方案便是注冊(cè)信號(hào)處理函數(shù),在 native 層可以用 sigaction():
#include <signal.h>
// signum:代表信號(hào)編碼,可以是除SIGKILL及SIGSTOP外的任何一個(gè)特定有效的信號(hào),如果為這兩個(gè)信號(hào)定義自己的處理函數(shù),將導(dǎo)致信號(hào)安裝錯(cuò)誤。
// act:指向結(jié)構(gòu)體sigaction的一個(gè)實(shí)例的指針,該實(shí)例指定了對(duì)特定信號(hào)的處理,如果設(shè)置為空,進(jìn)程會(huì)執(zhí)行默認(rèn)處理。
// oldact:和參數(shù)act類似,只不過保存的是原來對(duì)相應(yīng)信號(hào)的處理,也可設(shè)置為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 信號(hào).");
}
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 層的崩潰捕獲復(fù)雜就復(fù)雜在需要處理各種特殊情況,雖然一個(gè)函數(shù)就能監(jiān)聽到崩潰信號(hào)回調(diào),但是需要預(yù)防各種其他異常情況的出現(xiàn),我們一一來看下:
3.1 設(shè)置額外棧空間
SIGSEGV 很有可能是棧溢出引起的,如果在默認(rèn)的棧上運(yùn)行很有可能會(huì)破壞程序運(yùn)行的現(xiàn)場(chǎng),無法獲取到正確的上下文。而且當(dāng)棧滿了(太多次遞歸,棧上太多對(duì)象),系統(tǒng)會(huì)在同一個(gè)已經(jīng)滿了的棧上調(diào)用 SIGSEGV 的信號(hào)處理函數(shù),又再一次引起同樣的信號(hào)。我們應(yīng)該開辟一塊新的空間作為運(yùn)行信號(hào)處理函數(shù)的棧。可以使用 sigaltstack 在任意線程注冊(cè)一個(gè)可選的棧,保留一下在緊急情況下使用的空間。(系統(tǒng)會(huì)在危險(xiǎn)情況下把棧指針指向這個(gè)地方,使得可以在一個(gè)新的棧上運(yùn)行信號(hào)處理函數(shù))
/**
* 先創(chuàng)建一塊 sigaltstack ,因?yàn)橛锌赡苁怯啥褩R绯霭l(fā)出的信號(hào)
*/
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 處理
某些信號(hào)可能在之前已經(jīng)被安裝過信號(hào)處理函數(shù),而 sigaction 一個(gè)信號(hào)量只能注冊(cè)一個(gè)處理函數(shù),這意味著我們的處理函數(shù)會(huì)覆蓋其他人的處理信號(hào)。保存舊的處理函數(shù),在處理完我們的信號(hào)處理函數(shù)后,在重新運(yùn)行老的處理函數(shù)就能完成兼容。
/* Call the old handler. */
void call_old_signal_handler(const int sig, siginfo_t *const info, void *const sc) {
// 恢復(fù)默認(rèn)應(yīng)該也行吧
LOGD("sig -> %d", sig);
handlerMaps->at(sig)->sa_sigaction(sig, info, sc);
}
3.3 防止死鎖或者死循環(huán)
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) */
// 這里要考慮用非信號(hào)方式防止死鎖
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 堆棧
關(guān)于解析 native 層的 crash 堆棧解析,并不是一兩句話能說清楚的,因此我們打算單獨(dú)拿一次課來跟大家講。視頻鏈接地址無法發(fā)出來希望大家能夠諒解,因?yàn)橐徽迟N視頻地址文章就會(huì)被簡(jiǎn)書鎖定。大家感興趣的話,可以去我的 csdn 或者掘金找。