Linux線上系統程序debug思路及方法

       很多程序長期在線上系統跑着,可能跑着跑着就coredump了,而這種bug比較難復現,這個問題估計困擾不少同行朋友,這裏記錄一下我的一些思路,如有不對之處,歡迎指正。

1、coredump文件

       這個方法很基礎了,相信大家都知道,具體步驟如下:

ulimit -c unlimited
ulimit -c unlimited' >> /etc/profile
service apport stop
sysctl -w kernel.core_pattern=/var/log/%e.core.%p

       這幾個命令也不用多說,主要是開啓coredump以及指定coredump保存的路徑,當coredump文件產生時,用gdb指定二進制文件和coredump文件進去就可以調試了,比如bt查看調用堆棧、查看寄存器、反彙編、查看變量等等。這個方法在開發環境比較合適,在線上環境就要小心了,原因是coredump文件很大,一兩G都有可能,如果程序出現重複或者多次coredump那用不了多長時間硬盤就滿了,硬盤滿了之後一些重要的日誌(比如nginx的訪問日誌)就無法寫入了,所以這種方法在硬盤夠大或者只是偶爾出現coredump的情況下比較合適。

2、screen+gdb

       如果在關閉coredump的情況下,還有幾種方法可以使用,這裏先介紹screen+gdb的方法,這種方法也比較簡單,就是用gdb attach到可能出現coredump的進程,然後等待coredump的出現就可以在線調試了,但是得結合screen來使用,因爲我們上線上服務器一般都是ssh上去的,這個ssh連接可能比較容易斷掉,如果這個ssh連接被斷掉了,那之前gdb attach就白費了,所以結合screen來用就可以了。步驟:首先ssh連接服務器,其次screen打開一個shell,然後gdb attach到指定進程,最後就是等待coredump的出現了。也不是死等,就是每半天或者過一段時間來看看有沒有產生coredump就行。

       這個方法優點就是能直接在線調試,比較容易找出bug原因,但也缺點也是有的,因爲gdb上去之後肯定會影響程序的性能,二是發生coredump之後程序就卡在gdb那裏了,如果是nginx之類的程序,那可能一些請求就被超時了。這個方法就酌情使用吧。

3、內核日誌+反彙編

       如果沒有開啓coredump,而程序又死了,也沒法gdb attach上去調試。這種情況下,可以分析一下dmesg,比如下面有一個產生coredump的例子:

raise_coredump.c:

#include <stdio.h>

void func(char *p)
{
    *p = 'p';
}

int main(int argc, char *argv[])
{
    char *a;
    int  b;
    char *p = NULL;

    a = "aa";
    b = 22;

    func(p);

    return 0;
}
上面代碼中func函數修改了字符串參數p的第一個字符爲p,但它並沒有判斷參數p是否爲NULL,所以在main函數中調用func函數並傳一個NULL指針進去,肯定coredump,下面是編譯運行結果:

root@jusse ~/develop/debug_coredump# dmesg -c

root@jusse ~/develop/debug_coredump# gcc -Wall -g -o raise_coredump ./raise_coredump.c
./raise_coredump.c: In function ‘main’:
./raise_coredump.c:11:10: warning: variable ‘b’ set but not used [-Wunused-but-set-variable]
./raise_coredump.c:10:11: warning: variable ‘a’ set but not used [-Wunused-but-set-variable]

root@jusse ~/develop/debug_coredump# ./raise_coredump       
Segmentation fault

root@jusse ~/develop/debug_coredump# 
可見coredump已經產生了,調試方法就是dmesg和反彙編:


如圖所示,先是用dmesg看看內核日誌,ip後面跟的就是指令寄存器的值,也就是當前正在執行指令的地址,有了這個地址我們就可以用objdump來反彙編看看是哪條指令引起的coredump了,如果編譯時沒有優化也可以直接用addr2line來看是哪行代碼出了問題,如果嫌objdump+grep麻煩的話,也可以用gdb來反彙編:


可以看出gdb的反彙編也能直接顯示是在哪個函數以及相應的指令了。(這種方法應該不合適於程序加載地址隨機的情況,待驗證)。gdb的這個方式更適合於像調試nginx這種多進程,比如其中一個進程coredump重啓了,我們可以dmesg拿到地址之後,gdb attach到nginx的master進程中,再disassemble就可以查看相應的彙編指令了。

4、breakpad

       breakpad是google搞出來的一個東西,如果產生coredump,那breakpad將收集調用堆棧信息,然後用它自己的格式保存到文件中,我們可以把這個文件拿到本地分析。這種方法優點是能收集產生coredump時的調用堆棧,而且文件比較小,可以拿到本地分析,比較適合線上系統,缺點是收集的信息比較少,而且在C語言裏用的話還需要把它的C++庫進行一次封裝,比較麻煩。有興趣可以看看:http://blog.csdn.net/wpc320/article/details/8291296

5、處理SIGSEGV信號

       其實,我重點是想介紹這種方式,而且據瞭解,很多人也已經採用這種方式。原理就是:因爲coredump主要是由於一些非法操作導致產生SIGSEGV信號而引起的,所以在我們的程序中註冊SIGSEGV信號,當進程收到SIGSEGV信號時,在我們的信號處理函數中調用gdb來attach到自己的進程,然後就可以通過gdb來收集自己想要的信息了。例子如下:

segmentfault_handler.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>

void segment_fault_handler(int sig)
{
    char gdb_cmd[64] = {0};
    printf("segment_fault_handler\n");
    
    //組合gdb命令參數,gdb.cmd文件是gdb的命令腳本,gdb的輸出重定向到gdb_debug.log日誌文件中
    snprintf(gdb_cmd, sizeof(gdb_cmd)-1, "gdb -q -p %d -x ./gdb.cmd >gdb_debug.log 2>&1", getpid());
    system(gdb_cmd);
    signal(SIGSEGV, SIG_DFL);
}

void func(char *p)
{
    *p = 'p';//產生coredump
}

int main(int argc, char *argv[])
{
    printf("main start\n");
    char *p = NULL;

    //註冊SIGSEGV信號
    if (signal(SIGSEGV, segment_fault_handler) == SIG_ERR) {
        perror("signal error: ");
    }

    func(p);

    while (1) {
        sleep(10);
    }

    return 0;
}

編譯運行:


如圖所示,註冊了SIGSEGV信號處理函數之後,在處理函數中是可以啓gdb來收集進程死之前的一些關鍵信息的,因爲gdb的-x參數是指定命令腳本,所以就可以根據自己的需要來修改命令腳本就行了。這種方式感覺還不錯,你也可以試試。

如有其他更好的調試方法,歡迎指教~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章