中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

Android Native Crash 收集

2018-09-11    來(lái)源:編程學(xué)習(xí)網(wǎng)

容器云強(qiáng)勢(shì)上線!快速搭建集群,上萬(wàn)Linux鏡像隨意使用

本篇核心講解了自己實(shí)現(xiàn)一個(gè) Android Native Crash 收集的方案步驟,重點(diǎn)問(wèn)題解決辦法。

對(duì)本文有任何問(wèn)題,可加我的個(gè)人微信:kymjs123

在 Android 平臺(tái)上,Native Crash 一直是比較麻煩的問(wèn)題,因?yàn)椴东@麻煩,獲取到了內(nèi)容又不全,內(nèi)容全了信息又不對(duì),信息對(duì)了又不好處理。比 Java Crash 不知道麻煩多少倍。

今天跟大家講一下,我最近掉了幾百根頭發(fā)寫出來(lái)的一個(gè) Native Crash 收集的功能(脫發(fā)已經(jīng)越來(lái)越嚴(yán)重了)。

一個(gè) Native Crash 的 log 信息如下圖:

這張圖是我在網(wǎng)上找的(由于沒(méi)有寫 demo,項(xiàng)目中的截圖不方便直接拿出來(lái),就偷了個(gè)懶)。

在上圖里,堆棧信息中 pc 后面跟的內(nèi)存地址,就是當(dāng)前函數(shù)的棧地址,我們可以通過(guò)命令行 arm-linux-androideabi-addr2line -e 內(nèi)存地址 得出出錯(cuò)的代碼行數(shù)了。

要實(shí)現(xiàn) Native Crash 的收集,主要有四個(gè)重點(diǎn):知道 Crash 的發(fā)生;捕獲到 Crash 的位置;獲取 Crash 發(fā)生位置的函數(shù)調(diào)用棧;數(shù)據(jù)能回傳到服務(wù)器。

知道 Crash 的發(fā)生

與 Java 平臺(tái)不同,C/C++ 沒(méi)有一個(gè)通用的異常處理接口,在 C 層,CPU 通過(guò)異常中斷的方式,觸發(fā)異常處理流程。不同的處理器,有不同的異常中斷類型和中斷處理方式,linux 把這些中斷處理,統(tǒng)一為信號(hào)量,每一種異常都有一個(gè)對(duì)應(yīng)的信號(hào),可以注冊(cè)回調(diào)函數(shù)進(jìn)行處理需要關(guān)注的信號(hào)量。

所有的信號(hào)量都定義在<signal.h>文件中,這里我將幾乎全部的信號(hào)量以及所代表的含義都標(biāo)注出來(lái)了:

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

通常我們?cè)谧?crash 收集的時(shí)候,主要關(guān)注這幾個(gè)信號(hào)量:

const int signal_array[] = {SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT, SIGSYS};

對(duì)應(yīng)的含義可以參考上文,

extern int sigaction(int, const struct sigaction*, struct sigaction*);

第一個(gè)參數(shù) int 類型,表示需要關(guān)注的信號(hào)量

第二個(gè)參數(shù) sigaction 結(jié)構(gòu)體指針,用于聲明當(dāng)某個(gè)特定信號(hào)發(fā)生的時(shí)候,應(yīng)該如何處理。

第三個(gè)參數(shù)也是 sigaction 結(jié)構(gòu)體指針,他表示的是默認(rèn)處理方式,當(dāng)我們自定義了信號(hào)量處理的時(shí)候,用他存儲(chǔ)之前默認(rèn)的處理方式。

這也是指針與引用的區(qū)別,指針操作操作的都是變量本身,所以給新指針賦值了以后,需要另一個(gè)指針來(lái)記錄封裝了默認(rèn)處理方式的變量在內(nèi)存中的位置。

所以,要訂閱異常發(fā)生的信號(hào),最簡(jiǎn)單的做法就是直接用一個(gè)循環(huán)遍歷所有要訂閱的信號(hào),對(duì)每個(gè)信號(hào)調(diào)用 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]);
    }
}

捕獲到 Crash 的位置

sigaction 結(jié)構(gòu)體有一個(gè) sa_sigaction 變量,他是個(gè)函數(shù)指針,原型為: void (*)(int siginfo_t *, void *)
因此,我們可以聲明一個(gè)函數(shù),直接將函數(shù)的地址賦值給 sa_sigaction

void signal_handle(int code, siginfo_t *si, void *context) {
}

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]);
	}
}

這樣當(dāng)發(fā)生 Crash 的時(shí)候就會(huì)回調(diào)我們傳入的 signal_handle() 函數(shù)了。在 signal_handle() 函數(shù)中,我們得要想辦法拿到當(dāng)前執(zhí)行的代碼信息。

設(shè)置緊急?臻g

如果當(dāng)前函數(shù)發(fā)生了無(wú)限遞歸造成堆棧溢出,在統(tǒng)計(jì)的時(shí)候需要考慮到這種情況而新開堆棧否則本來(lái)就滿了的堆棧又在當(dāng)前堆棧處理溢出信號(hào),處理肯定是會(huì)失敗的。所以我們需要設(shè)置一個(gè)用于緊急處理的新棧,可以使用 sigaltstack() 在任意線程注冊(cè)一個(gè)可選的棧,保留一下在緊急情況下使用的空間。(系統(tǒng)會(huì)在危險(xiǎn)情況下把棧指針指向這個(gè)地方,使得可以在一個(gè)新的棧上運(yùn)行信號(hào)處理函數(shù))

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);
}

捕獲出問(wèn)題的代碼

signal_handle() 函數(shù)中的第三個(gè)參數(shù) context 是 uc_mcontext 的結(jié)構(gòu)體指針,它封裝了 cpu 相關(guān)的上下文,包括當(dāng)前線程的寄存器信息和奔潰時(shí)的 pc 值,能夠知道崩潰時(shí)的pc,就能知道崩潰時(shí)執(zhí)行的是那條指令,同樣的,在本文頂部的那張圖中寄存器快照就可以用如下代碼獲得。

char *head_cpu = nullptr;
asprintf(&head_cpu, "r0 %08lx  r1 %08lx  r2 %08lx  r3 %08lx\n"
                 "r4 %08lx  r5 %08lx  r6 %08lx  r7 %08lx\n"
                 "r8 %08lx  r9 %08lx  sl %08lx  fp %08lx\n"
                 "ip %08lx  sp %08lx  lr %08lx  pc %08lx  cpsr %08lx\n",
         t->uc_mcontext.arm_r0, t->uc_mcontext.arm_r1, t->uc_mcontext.arm_r2,
         t->uc_mcontext.arm_r3, t->uc_mcontext.arm_r4, t->uc_mcontext.arm_r5,
         t->uc_mcontext.arm_r6, t->uc_mcontext.arm_r7, t->uc_mcontext.arm_r8,
         t->uc_mcontext.arm_r9, t->uc_mcontext.arm_r10, t->uc_mcontext.arm_fp,
         t->uc_mcontext.arm_ip, t->uc_mcontext.arm_sp, t->uc_mcontext.arm_lr,
         t->uc_mcontext.arm_pc, t->uc_mcontext.arm_cpsr);

不過(guò) uc_mcontext 結(jié)構(gòu)體的定義是平臺(tái)相關(guān)的,比如我們熟知的 arm 、 x86 這種都不是同一個(gè)結(jié)構(gòu)體定義,上面的代碼只列出了 arm 架構(gòu)的寄存器信息,要兼容其他架構(gòu)的 cpu 在處理的時(shí)候,就得要寄出宏編譯大法,不同的架構(gòu)使用不同的定義。

uintptr_t pc_from_ucontext(const ucontext_t *uc) {
#if (defined(__arm__))
    return uc->uc_mcontext.arm_pc;
#elif defined(__aarch64__)
    return uc->uc_mcontext.pc;
#elif (defined(__x86_64__))
    return uc->uc_mcontext.gregs[REG_RIP];
#elif (defined(__i386))
  return uc->uc_mcontext.gregs[REG_EIP];
#elif (defined (__ppc__)) || (defined (__powerpc__))
  return uc->uc_mcontext.regs->nip;
#elif (defined(__hppa__))
  return uc->uc_mcontext.sc_iaoq[0] & ~0x3UL;
#elif (defined(__sparc__) && defined (__arch64__))
  return uc->uc_mcontext.mc_gregs[MC_PC];
#elif (defined(__sparc__) && !defined (__arch64__))
  return uc->uc_mcontext.gregs[REG_PC];
#else
#error "Architecture is unknown, please report me!"
#endif
}

pc值轉(zhuǎn)內(nèi)存地址

pc值是程序加載到內(nèi)存中的絕對(duì)地址,絕對(duì)地址不能直接使用,因?yàn)槊看纬绦蜻\(yùn)行創(chuàng)建的內(nèi)存肯定都不是固定區(qū)域的內(nèi)存,所以絕對(duì)地址肯定每次運(yùn)行都不一致。我們需要拿到崩潰代碼相對(duì)于當(dāng)前庫(kù)的相對(duì)偏移地址,這樣才能使用 addr2line 分析出是哪一行代碼。通過(guò) dladdr() 可以獲得共享庫(kù)加載到內(nèi)存的起始地址,和 pc 值相減就可以獲得相對(duì)偏移地址,并且可以獲得共享庫(kù)的名字。

Dl_info info;  
if (dladdr(addr, &info) && info.dli_fname) {  
  void * const nearest = info.dli_saddr;  
  uintptr_t addr_relative = addr - info.dli_fbase;  
}

獲取 Crash 發(fā)生時(shí)的函數(shù)調(diào)用棧

獲取函數(shù)調(diào)用棧是最麻煩的,至今沒(méi)有一個(gè)好用的,全都要做一些大改動(dòng)。常見的做法有四種:

  • 第一種:直接使用系統(tǒng)的 <unwind.h> 庫(kù),可以獲取到出錯(cuò)文件與函數(shù)名。只不過(guò)需要自己解析函數(shù)符號(hào),同時(shí)經(jīng)常會(huì)捕獲到系統(tǒng)錯(cuò)誤,需要手動(dòng)過(guò)濾。
  • 第二種:在 4.1.1 以上, 5.0 以下,使用系統(tǒng)自帶的 libcorkscrew.so ,5.0開始,系統(tǒng)中沒(méi)有了 libcorkscrew.so ,可以自己編譯系統(tǒng)源碼中的 libunwind 。 libunwind 是一個(gè)開源庫(kù),事實(shí)上高版本的安卓源碼中就使用了他的優(yōu)化版替換 libcorkscrew 。
  • 第三種:使用開源庫(kù) coffeecatch ,但是這種方案也不能百分之百兼容所有機(jī)型。
  • 第四種:使用 Google 的 breakpad ,這是所有 C/C++堆棧獲取的權(quán)威方案,基本上業(yè)界都是基于這個(gè)庫(kù)來(lái)做的。只不過(guò)這個(gè)庫(kù)是全平臺(tái)的 android、iOS、Windows、Linux、MacOS 全都有,所以非常大,在使用的時(shí)候得把無(wú)關(guān)的平臺(tái)剝離掉減小體積。

下面以第一種為例講一下實(shí)現(xiàn):

核心方法是使用 <unwind.h> 庫(kù)提供的一個(gè)方法 _Unwind_Backtrace() 這個(gè)函數(shù)可以傳入一個(gè)函數(shù)指針作為回調(diào),指針指向的函數(shù)有一個(gè)重要的參數(shù)是 _Unwind_Context 類型的結(jié)構(gòu)體指針。

可以使用 _Unwind_GetIP() 函數(shù)將當(dāng)前函數(shù)調(diào)用棧中每個(gè)函數(shù)的絕對(duì)內(nèi)存地址(也就是上文中提到的 pc 值),寫入到 _Unwind_Context 結(jié)構(gòu)體中,最終返回的是當(dāng)前調(diào)用棧的全部函數(shù)地址了, _Unwind_Word 實(shí)際上就是一個(gè) unsigned int 。

而 capture_backtrace() 返回的就是當(dāng)前我們獲取到調(diào)用棧中內(nèi)容的數(shù)量。

/**
 * callback used when using <unwind.h> to get the trace for the current context
 */
_Unwind_Reason_Code unwind_callback(struct _Unwind_Context *context, void *arg) {
    backtrace_state_t *state = (backtrace_state_t *) arg;
    _Unwind_Word pc = _Unwind_GetIP(context);
    if (pc) {
        if (state->current == state->end) {
            return _URC_END_OF_STACK;
        } else {
            *state->current++ = (void *) pc;
        }
    }
    return _URC_NO_REASON;
}

/**
 * uses built in <unwind.h> to get the trace for the current context
 */
size_t capture_backtrace(void **buffer, size_t max) {
    backtrace_state_t state = {buffer, buffer + max};
    _Unwind_Backtrace(unwind_callback, &state);
    return state.current - buffer;
}

當(dāng)所有的函數(shù)的絕對(duì)內(nèi)存地址(pc 值)都獲取到了,就可以用上文講的辦法將 pc 值轉(zhuǎn)換為相對(duì)偏移量,獲取到真正的函數(shù)信息和相對(duì)內(nèi)存地址了。

void *buffer[max_line];
int frames_size = capture_backtrace(buffer, max_line);
for (int i = 0; i < frames_size; i++) {
	Dl_info info;  
	const void *addr = buffer[i];
	if (dladdr(addr, &info) && info.dli_fname) {  
	  void * const nearest = info.dli_saddr;  
	  uintptr_t addr_relative = addr - info.dli_fbase;  
}

Dl_info 是一個(gè)結(jié)構(gòu)體,內(nèi)部封裝了函數(shù)所在文件、函數(shù)名、當(dāng)前庫(kù)的基地址等信息

typedef struct {
    const char *dli_fname;  /* Pathname of shared object that
                               contains address */
    void       *dli_fbase;  /* Address at which shared object
                               is loaded */
    const char *dli_sname;  /* Name of nearest symbol with address
                               lower than addr */
    void       *dli_saddr;  /* Exact address of symbol named
                               in dli_sname */
} Dl_info;

有了這個(gè)對(duì)象,我們就能獲取到全部想要的信息了。雖然獲取到全部想要的信息,但 <unwind.h> 有個(gè)麻煩的就是不想要的信息也給你了,所以需要手動(dòng)過(guò)濾掉各種系統(tǒng)錯(cuò)誤,最終得到的數(shù)據(jù),就可以上報(bào)到自己的服務(wù)器了。

數(shù)據(jù)回傳到服務(wù)器

數(shù)據(jù)回傳有兩種方式,一種是直接將信息寫入文件,下次啟動(dòng)的時(shí)候直接由 Java 上報(bào);另一種就是回調(diào) Java 代碼,讓 Java 去處理。用 Java 處理的好處是 Java 層可以繼續(xù)在當(dāng)前上下文上加上 Java 層的各種狀態(tài)信息,寫入到同一個(gè)文件中,使得開發(fā)在解決 bug 的時(shí)候能更方便。

這里就簡(jiǎn)單將數(shù)據(jù)寫入文件了。

void save(const char *name, char *content) {
    FILE *file = fopen(name, "w+");
    fputs(content, file);
    fflush(file);
    fclose(file);
    //可以在寫入文件以后,再通知 Java 層,直接將文件名傳給 Java 層更簡(jiǎn)單。  
    report();
}

如果你按照本文講的,應(yīng)該是可以創(chuàng)建一個(gè)可以工作的 Native Crash 收集庫(kù)了,但是還有很多細(xì)節(jié)上的問(wèn)題,比如數(shù)據(jù)的丟失問(wèn)題,寫文件的時(shí)候使用 w+ 可能造成上次存儲(chǔ)的文件丟失;如果當(dāng)前函數(shù)發(fā)生了無(wú)限遞歸造成堆棧溢出,在統(tǒng)計(jì)的時(shí)候需要考慮到這種情況而新開堆棧否則本來(lái)就滿了的堆棧又在當(dāng)前堆棧處理溢出信號(hào),處理肯定是會(huì)失敗的;再比方說(shuō)多進(jìn)程多線程在 C 上的各種問(wèn)題,真的是很復(fù)雜。

 

來(lái)自:https://www.kymjs.com/code/2018/08/22/01/

 

標(biāo)簽: Google linux 代碼 服務(wù)器 權(quán)限 通信

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請(qǐng)聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請(qǐng)與原作者聯(lián)系。

上一篇:Android 開發(fā)技術(shù)周報(bào) Issue#192

下一篇:Linux 查看進(jìn)程消耗內(nèi)存情況總結(jié)