寫代碼的人最難堪而又無法回避的事情之一,莫過於你寫的程序某刻當着 QA 的面突然掛掉 --- 大大沒面子!但更沒面子的是,之后你一直沒法解決問題。。。程序崩潰而又無法解決可能有很多的原因,其中一個就是無法找到出問題的地方,尤其是那些 release 版本的程序。異常崩潰后的善后處理是一件很重要而又不大好做的事情,一方面事關用戶體驗,另一方面能否盡可能收集崩潰現場的信息關系着接下來能否快速和及時有效地解決問題。
google breakpad 可以說算是專門為解決這類事情而開發出來的工具。簡單來說,它的主要作用是在程序崩潰后,輸出一個特殊的 coredump,並且然后根據 coredump 還原崩潰時函數的調用棧,為了做到這些它提供了一些手段,使得你的程序能夠捕獲異常,從而能讓它在彌留之際可以煽情地留下些什么,而不是突然就憑空消失了。
具體的介紹讀者可以參考一下官方的說明:https://code.google.com/p/google-breakpad/wiki/GettingStartedWithBreakpad. 本文只簡單記一下怎樣在我們的代碼中使用該工具,后續會探討一下實現的細節。
使用流程:
a. 代碼中加入 breakpad 模塊(后面介紹)。
b. 加 -g 編譯程序,然后用 breakpad 提供的工具 dump_syms 從編譯好的可執行文件中 dump 出一個符號文件,dump 的時候需指定相應的程序,及最后輸出的符號文件的名字,如下:
dump_syms ./test > test.sym
如果你的程序需要鏈接其它的動態鏈接庫(比如libc.so,其它第三方或自己的動態庫),則最好也同樣把這些動態庫的符號dump下來,程序崩潰時函數的調用鏈穿過動態庫這屬於太正常的現象了,如果沒有這些相應的動態庫的符號,還原不出庫里的函數的名字還是小事,更麻煩的是有時因為缺失這些符號信息,常常搞得調用鏈上其它有符號的函數也沒法正確的還原出來。
在完成dump符號后,你就可以把程序的 debug 信息 strip 掉了,這個特性可以說是與 coredump 相比最大一亮點,dump 完 symbol 后 strip 完全不影響后續的崩潰現場還原。
c. 發布程序,用戶在使用程序的過程中,如果不幸發生了崩潰,breakpad 會及時產生 mini dump,我們需要把這個 mini dump 拿回來,配合前面 dump 出來的符號文件,再在 breakpad 提供的工具 minidump_stackwalk的幫助下,我們可以還原出程序崩潰那一刻的 call stack。
minidump_stackwalk 在使用上有些麻煩,需要如下步驟:
1.需要先把前面dump下來的符號文件改一下名字,改為符號文件中第一行的倒數第二個字符,這是一長串的16進制字符串。
2.然后把這個符號文件移到./symbols/test/ 目錄下,test是你的程序的名字。
3. 動庫也需要做同樣的處理,放到同一目錄下或放到別的目錄都可以。
$ head -n1 test.sym
MODULE Linux x86_64 6EDC6ACDB282125843FD59DA9C81BD830 test
$ mkdir -p ./symbols/test/6EDC6ACDB282125843FD59DA9C81BD830
$ mv test.sym ./symbols/test/6EDC6ACDB282125843FD59DA9C81BD830
$ head -n1 libc.sym
MODULE Linux x86_64 G4YCJAGNB282895843FD59DA9C81BD830 libc.so
$ mkdir -p ./symbols/libc.so/G4YCJAGNB282895843FD59DA9C81BD830
$ mv libc.so.sym ./symbols/libc.so/G4YCJAGNB282895843FD59DA9C81BD830
4. 最后運行:
minidump_stackwalk minidump.dmp ./symbols
上面的步聚如果過於麻煩,你可以自己修改一下dump_syms這個工具,dump的時候就把目錄結構給建好了,免得手動操作,或者也可以借用一下火狐提供的一個python寫的工具,一步到位:
http://mxr.mozilla.org/mozilla-central/source/toolkit/crashreporter/tools/symbolstore.py
怎樣把 breakpad 加入程序中:
1) 編譯breakpad.
按照說明進行編譯后,會得到一個libbreakpad_client.a 及一系列的工具。其中最主要的兩個在:
a. /src/tools/linux/dump_syms/dump_syms
b. /src/processor/minidump_stackwalk
有一點需要提一下,這個庫在低版本的gcc中編譯不了,比較不方便,google code上有人提了這個問題,也有人做了些patch,但由於庫在實現上依賴了一些高版本編譯器的特性,作者並不打算去解決這個問題了。
2) 把 libbreakpad_client.a 鏈接到你的程序中.
3) 至於怎么在你的程序加以使用,其實非常簡單,示例如下:
#include "client/linux/handler/exception_handler.h"
#include "third_party/lss/linux_syscall_support.h"
#include <sys/types.h> #include <unistd.h> #include <stdio.h> static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descr, void*context,bool succeeded) { printf("crash occurs!\n"); pid_t ret = sys_fork(); if(ret < 0) { fprintf(stderr,"mini dump fork error/n"); return false; } else if(ret == 0) { printf("child process\n");
char* const arg[] = {"ch",(char *)descr.path(),NULL};
char* const env[] = {NULL,NULL};
sys_execve("./ch",arg,env);
// execl("./dh","dh",descr.path(),0); printf("error , we should never be here\n"); } else { } return true; } int main() { printf("crash source.\n"); using namespace google_breakpad; MinidumpDescriptor desc("./dump"); ExceptionHandler eh(desc,NULL,dumpCallback,NULL,true,-1); printf("ready to crash\n"); int num = 0; int div = 3; int i = div/num; return 0; }
一點解釋
現在我們來看看 ExceptionHandler 的原型。
// Creates a new ExceptionHandler instance to handle writing minidumps. 119 // Before writing a minidump, the optional |filter| callback will be called. 120 // Its return value determines whether or not Breakpad should write a 121 // minidump. The minidump content will be written to the file path or file 122 // descriptor from |descriptor|, and the optional |callback| is called after 123 // writing the dump file, as described above. 124 // If install_handler is true, then a minidump will be written whenever 125 // an unhandled exception occurs. If it is false, minidumps will only 126 // be written when WriteMinidump is called. 127 // If |server_fd| is valid, the minidump is generated out-of-process. If it 128 // is -1, in-process generation will always be used. 129 ExceptionHandler(const MinidumpDescriptor& descriptor, 130 FilterCallback filter, 131 MinidumpCallback callback, 132 void *callback_context, 133 bool install_handler, 134 const int server_fd);
可見,官方已經把功能做得相當傻瓜,使用者只要定義一個ExceptionHandler 的對象,傳入幾個 callback,這些 callback 就會在異常出現時被適時調用,這些 callback 主要包括:
1) FilterCallback,過濾一下,該處理哪些異常。
2) MiniCallback,在異常發生,且 mini coredump 已經被輸出后,這個 callback 會被調用。
// If a FilterCallback returns true, Breakpad will continue processing, 84 // attempting to write a minidump. If a FilterCallback returns false, 85 // Breakpad will immediately report the exception as unhandled without 86 // writing a minidump, allowing another handler the opportunity to handle it. 87 typedef bool (*FilterCallback)(void *context); 88 89 // A callback function to run after the minidump has been written. 90 // |descriptor| contains the file descriptor or file path containing the 91 // minidump. |context| is the parameter supplied by the user as 92 // callback_context when the handler was created. |succeeded| indicates 93 // whether a minidump file was successfully written. 94 // 95 // If an exception occurred and the callback returns true, Breakpad will 96 // treat the exception as fully-handled, suppressing any other handlers from 97 // being notified of the exception. If the callback returns false, Breakpad 98 // will treat the exception as unhandled, and allow another handler to handle 99 // it. If there are no other handlers, Breakpad will report the exception to 100 // the system as unhandled, allowing a debugger or native crash dialog the 101 // opportunity to handle the exception. Most callback implementations 102 // should normally return the value of |succeeded|, or when they wish to 103 // not report an exception of handled, false. Callbacks will rarely want to 104 // return true directly (unless |succeeded| is true). 105 typedef bool (*MinidumpCallback)(const MinidumpDescriptor& descriptor, 106 void* context, 107 bool succeeded);
需要注意的是,在異常出現后,崩潰的進程常常已經被破壞了,因此在這個 callback 里不要做太多事情,特別是內存訪問,寫入以及調用別的庫的函數等,這些很難保證能正常工作。
Note: You should do as little work as possible in the callback function. Your application is in an unsafe state. It may not be safe to allocate memory or call functions from other shared libraries.
這里特指不要調用動態鏈接庫中的函數,原因有如下幾個:
1. 有的動態庫此時可能還沒完全被加載完畢(如符號重定位),在這種時候調用其中的函數會觸發動態動態庫運行時加載的一系列動作,這在程序已經崩潰的情況下通常做不到。
2. 動態庫中的函數可能會引用 heap 上某些內存,而程序崩潰時,這些內存有可能被破壞了,庫中函數未必能正常工作。
3. 對某些 libc 中的函數,它們是有鎖的,程序崩潰時,其它線程都會被暫停,此時如果某些線程恰好也調用了這些函數並取得了鎖,那么在信號處理函數中就無法再獲得鎖,因此會死鎖。
唯一的解決方法是繞過 libc 直接通過 syscall 來與 kernel 打交道,被推薦的做法是,直接在 callback 里 fork 一個子進程出來,exec 新的程序,再進行異常處理。有人也許會問,fork,exec 等常用 POSIX 接口,不就是在動態庫中嗎? 是的!所以我們只好把它們拋棄!但我們對它們是這樣的熟悉,以至於依賴,如果完全不用這些 libc,glibc 中的函數,人生簡直失去了意義。。。
人生可以沒意義,事情還是要做的,脫離了glibc,libc 雖然難受,但還不至於活不下去。對於很多如 fork,exec 這類的系統調用來說,glibc 只是對它們的進行了一個包裝,我們完全可以直接通過 syscall 一系列函數來直接與內核打交道。
唯一的問題是,凡事都要自己動手,人生沒意義之外,還未免太痛苦。
這里 google 又幫了點小忙,你直接可以引入一個頭文件:lss/linux_syscall_support.h, 該頭文件提供了很多的直接進行系統調用的宏與函數(其實就是那一堆 syscall 的封裝),做的很貼心,很多我們用到系統調用,它都包裝成了類似的名字,如常用的 fork() ,它就包裝了一個 sys_fork() 代替。換而言之,在所有用到 POSIX 接口中的函數地方,你全部加上 sys_ 開頭,基本就能直接替換,如果不幸哪些函數沒有,那就只能自己動手來 syscall 了。
友情提示,exec 系列函數,linux_syscall_support.h 中就只提供了一個 sys_execve,別的如 execl 等都沒有定義。
除了這些系統調用相關的,google breakpad 也提供了一些字符串操作函數用來替代 libc 中常用諸如 strcpy,strlen 之類的函數,但是數量非常有限,這些替代函數可以在頭文件: common/ linux/ linux_libc_support.h 里找到。上述代碼中,exec 的程序如下:
#include<unistd.h> #include<stdio.h> #include<sys/stat.h> #include<sys/types.h> static void Reporter(const char* path) { printf("crash reporter process.\n"); printf("dump file path:%s\n",path); } int main(int arg,char* argv[]) { if(arg == 2) { Reporter(argv[1]); } }
顯然,這里只是簡單地把 coredump 的路徑打印了一下,功能完善的 crash reporter 應該在這里起一個GUI, 詢問一下用戶可否願意上傳 coredump 及描述一下崩潰時的相關操作信息,並處理上傳的邏輯。
breakpad 還很貼心地提供了一個通過 http 來上傳 coredump 的工具,在如下路徑中:src/tools/linux/symupload/ 。這個工具的使用也是相當方便的,只需要搭起一個簡易的 http server,在客戶端設置一下用來接收coredump 的 server 的 url,及 coredump 的路徑就可以直接使用。 當然,如果你的上傳邏輯需要定制更多細節的話,就需要自己在它的基礎加以修改了。
結語
工欲就其事,必先利其器,谷歌一系列的開源工具,讓不少開發者受益匪淺,應當表揚一下。開源軟件在提高大眾軟件開發效率和生產應用上的貢獻,確實不可磨滅。只是喝水不忘挖井人才好,開源,人類的希望!