电脑知识|欧美黑人一区二区三区|软件|欧美黑人一级爽快片淫片高清|系统|欧美黑人狂野猛交老妇|数据库|服务器|编程开发|网络运营|知识问答|技术教程文章 - 好吧啦网

您的位置:首頁技術文章
文章詳情頁

Android Native Crash 收集

瀏覽:14日期:2022-09-05 13:15:22

本篇核心講解了自己實現一個 Android Native Crash 收集的方案步驟,重點問題解決辦法。

對本文有任何問題,可加我的個人微信:kymjs123

在 Android 平臺上,Native Crash 一直是比較麻煩的問題,因為捕獲麻煩,獲取到了內容又不全,內容全了信息又不對,信息對了又不好處理。比 Java Crash 不知道麻煩多少倍。

今天跟大家講一下,我最近掉了幾百根頭發寫出來的一個 Native Crash 收集的功能(脫發已經越來越嚴重了)。

一個 Native Crash 的 log 信息如下圖:

Android Native Crash 收集

這張圖是我在網上找的(由于沒有寫 demo,項目中的截圖不方便直接拿出來,就偷了個懶)。

在上圖里,堆棧信息中 pc 后面跟的內存地址,就是當前函數的棧地址,我們可以通過命令行 arm-linux-androideabi-addr2line -e 內存地址 得出出錯的代碼行數了。

要實現 Native Crash 的收集,主要有四個重點:知道 Crash 的發生;捕獲到 Crash 的位置;獲取 Crash 發生位置的函數調用棧;數據能回傳到服務器。

知道 Crash 的發生

與 Java 平臺不同,C/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 // 非法的系統調用

通常我們在做 crash 收集的時候,主要關注這幾個信號量:

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

對應的含義可以參考上文,

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

第一個參數 int 類型,表示需要關注的信號量

第二個參數 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]); }} 捕獲到 Crash 的位置

sigaction 結構體有一個 sa_sigaction 變量,他是個函數指針,原型為: void (*)(int siginfo_t *, void *) 因此,我們可以聲明一個函數,直接將函數的地址賦值給 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]);}}

這樣當發生 Crash 的時候就會回調我們傳入的 signal_handle() 函數了。在 signal_handle() 函數中,我們得要想辦法拿到當前執行的代碼信息。

設置緊急棧空間

如果當前函數發生了無限遞歸造成堆棧溢出,在統計的時候需要考慮到這種情況而新開堆棧否則本來就滿了的堆棧又在當前堆棧處理溢出信號,處理肯定是會失敗的。所以我們需要設置一個用于緊急處理的新棧,可以使用 sigaltstack() 在任意線程注冊一個可選的棧,保留一下在緊急情況下使用的空間。(系統會在危險情況下把棧指針指向這個地方,使得可以在一個新的棧上運行信號處理函數)

void signal_handle(int sig) { write(2, 'stack overflown', 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);}

捕獲出問題的代碼

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

char *head_cpu = nullptr;asprintf(&head_cpu, 'r0 %08lx r1 %08lx r2 %08lx r3 %08lxn' 'r4 %08lx r5 %08lx r6 %08lx r7 %08lxn' 'r8 %08lx r9 %08lx sl %08lx fp %08lxn' 'ip %08lx sp %08lx lr %08lx pc %08lx cpsr %08lxn', 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);

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

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值轉內存地址

pc值是程序加載到內存中的絕對地址,絕對地址不能直接使用,因為每次程序運行創建的內存肯定都不是固定區域的內存,所以絕對地址肯定每次運行都不一致。我們需要拿到崩潰代碼相對于當前庫的相對偏移地址,這樣才能使用 addr2line 分析出是哪一行代碼。通過 dladdr() 可以獲得共享庫加載到內存的起始地址,和 pc 值相減就可以獲得相對偏移地址,并且可以獲得共享庫的名字。

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 發生時的函數調用棧

獲取函數調用棧是最麻煩的,至今沒有一個好用的,全都要做一些大改動。常見的做法有四種:

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

下面以第一種為例講一下實現:

核心方法是使用 <unwind.h> 庫提供的一個方法 _Unwind_Backtrace() 這個函數可以傳入一個函數指針作為回調,指針指向的函數有一個重要的參數是 _Unwind_Context 類型的結構體指針。

可以使用 _Unwind_GetIP() 函數將當前函數調用棧中每個函數的絕對內存地址(也就是上文中提到的 pc 值),寫入到 _Unwind_Context 結構體中,最終返回的是當前調用棧的全部函數地址了, _Unwind_Word 實際上就是一個 unsigned int 。

而 capture_backtrace() 返回的就是當前我們獲取到調用棧中內容的數量。

/** * 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;}

當所有的函數的絕對內存地址(pc 值)都獲取到了,就可以用上文講的辦法將 pc 值轉換為相對偏移量,獲取到真正的函數信息和相對內存地址了。

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 是一個結構體,內部封裝了函數所在文件、函數名、當前庫的基地址等信息

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;

有了這個對象,我們就能獲取到全部想要的信息了。雖然獲取到全部想要的信息,但 <unwind.h> 有個麻煩的就是不想要的信息也給你了,所以需要手動過濾掉各種系統錯誤,最終得到的數據,就可以上報到自己的服務器了。

數據回傳到服務器

數據回傳有兩種方式,一種是直接將信息寫入文件,下次啟動的時候直接由 Java 上報;另一種就是回調 Java 代碼,讓 Java 去處理。用 Java 處理的好處是 Java 層可以繼續在當前上下文上加上 Java 層的各種狀態信息,寫入到同一個文件中,使得開發在解決 bug 的時候能更方便。

這里就簡單將數據寫入文件了。

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

如果你按照本文講的,應該是可以創建一個可以工作的 Native Crash 收集庫了,但是還有很多細節上的問題,比如數據的丟失問題,寫文件的時候使用 w+ 可能造成上次存儲的文件丟失;如果當前函數發生了無限遞歸造成堆棧溢出,在統計的時候需要考慮到這種情況而新開堆棧否則本來就滿了的堆棧又在當前堆棧處理溢出信號,處理肯定是會失敗的;再比方說多進程多線程在 C 上的各種問題,真的是很復雜。

來自:https://www.kymjs.com/code/2018/08/22/01/

標簽: Android
相關文章:
主站蜘蛛池模板: 软文发布-新闻发布推广平台-代写文章-网络广告营销-自助发稿公司媒介星 | 亚克隆,RNAi干扰检测,miRNA定量检测-上海基屹生物科技有限公司 | 茶楼装修设计_茶馆室内设计效果图_云臻轩茶楼装饰公司 | 盐水蒸发器,水洗盐设备,冷凝结晶切片机,转鼓切片机,絮凝剂加药系统-无锡瑞司恩机械有限公司 | 回转炉,外热式回转窑,回转窑炉-淄博圣元窑炉工程有限公司 | 创客匠人-让IP变现不走弯路 | 干培两用箱-细菌恒温培养箱-菲斯福仪器 | 飞象网 - 通信人每天必上的网站| 美国PARKER齿轮泵,美国PARKER柱塞泵,美国PARKER叶片泵,美国PARKER电磁阀,美国PARKER比例阀-上海维特锐实业发展有限公司二部 | IP检测-检测您的IP质量 | 广州冷却塔维修厂家_冷却塔修理_凉水塔风机电机填料抢修-广东康明节能空调有限公司 | 环讯传媒,永康网络公司,永康网站建设,永康小程序开发制作,永康网站制作,武义网页设计,金华地区网站SEO优化推广 - 永康市环讯电子商务有限公司 | 冷柜风机-冰柜电机-罩极电机-外转子风机-EC直流电机厂家-杭州金久电器有限公司 | 口臭的治疗方法,口臭怎么办,怎么除口臭,口臭的原因-口臭治疗网 | 碳纤维复合材料制品生产定制工厂订制厂家-凯夫拉凯芙拉碳纤维手机壳套-碳纤维雪茄盒外壳套-深圳市润大世纪新材料科技有限公司 | 成都亚克力制品,PVC板,双色板雕刻加工,亚克力门牌,亚克力标牌,水晶字雕刻制作-零贰捌广告 | 硅PU球场、篮球场地面施工「水性、环保、弹性」硅PU材料生产厂家-广东中星体育公司 | 防水试验机_防水测试设备_防水试验装置_淋雨试验箱-广州岳信试验设备有限公司 | 仿古建筑设计-仿古建筑施工-仿古建筑公司-汉匠古建筑设计院 | 二手Sciex液质联用仪-岛津气质联用仪-二手安捷伦气质联用仪-上海隐智科学仪器有限公司 | 淘趣英语网 - 在线英语学习,零基础英语学习网站 | 高效节能电机_伺服主轴电机_铜转子电机_交流感应伺服电机_图片_型号_江苏智马科技有限公司 | 穿线管|波纹穿线管|包塑金属软管|蛇皮管?闵彬专注弱电工程? | 铆钉机|旋铆机|东莞旋铆机厂家|鸿佰专业生产气压/油压/自动铆钉机 | 福州甲醛检测-福建室内空气检测_环境检测_水质检测-福建中凯检测技术有限公司 | 高铝矾土熟料_细粉_骨料_消失模_铸造用铝矾土_铝酸钙粉—嵩峰厂家 | PCB厂|线路板厂|深圳线路板厂|软硬结合板厂|电路板生产厂家|线路板|深圳电路板厂家|铝基板厂家|深联电路-专业生产PCB研发制造 | 西安微信朋友圈广告投放_微信朋友圈推广_西安度娘网络科技有限公司 | 重庆磨床过滤机,重庆纸带过滤机,机床伸缩钣金,重庆机床钣金护罩-重庆达鸿兴精密机械制造有限公司 | 杭州中央空调维修_冷却塔/新风机柜/热水器/锅炉除垢清洗_除垢剂_风机盘管_冷凝器清洗-杭州亿诺能源有限公司 | 【官网】博莱特空压机,永磁变频空压机,螺杆空压机-欧能优 | 爱科技iMobile-专业的科技资讯信息分享网站 | 电缆故障测试仪_电缆故障定位仪_探测仪_检测仪器_陕西意联电气厂家 | 精密模具制造,注塑加工,吹塑和吹瓶加工,EPS泡沫包装生产 - 济南兴田塑胶有限公司 | 合肥废气治理设备_安徽除尘设备_工业废气处理设备厂家-盈凯环保 合肥防火门窗/隔断_合肥防火卷帘门厂家_安徽耐火窗_良万消防设备有限公司 | 恒温恒湿试验箱_高低温试验箱_恒温恒湿箱-东莞市高天试验设备有限公司 | 安徽合肥格力空调专卖店_格力中央空调_格力空调总经销公司代理-皖格制冷设备 | 木材烘干机,木炭烘干机,纸管/佛香烘干设备-河南蓝天机械制造有限公司 | 道康宁消泡剂-瓦克-大川进口消泡剂供应商 | CNC机加工-数控加工-精密零件加工-ISO认证厂家-鑫创盟 | 铝镁锰板_铝镁锰合金板_铝镁锰板厂家_铝镁锰金属屋面板_安徽建科 |