<menu id="w8yyk"><menu id="w8yyk"></menu></menu>
  • <dd id="w8yyk"><nav id="w8yyk"></nav></dd>
    <menu id="w8yyk"></menu>
    <menu id="w8yyk"><code id="w8yyk"></code></menu>
    <menu id="w8yyk"></menu>
    <xmp id="w8yyk">
    <xmp id="w8yyk"><nav id="w8yyk"></nav>
  • 網站首頁 > 物聯資訊 > 技術分享

    用gdb調試程序筆記: 以段錯誤(Segmental fault)為例

    2016-09-28 00:00:00 廣州睿豐德信息科技有限公司 閱讀
    睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接 用gdb調試程序筆記: 以段錯誤(Segmental fault)為例[轉]

    1.背景介紹
    2.程序中常見的bug分類
    3.程序調試器(如gdb)有什么用
    4.段錯誤(Segmental fault)介紹
    5.gdb調試入門

     一、背景介紹
    這個筆記主要介紹開源的程序調試器(gdb)的入門知識,目的是使unix/linux環境的編程新手能夠快速學會使用gdb調試程序的方法,同時也是對我使用gdb的一個經驗總結。
    本文假設你能使用簡單的unix/linux命令并能用gcc(GNU C Compiler, GNU C 語言編譯器)編譯程序,當然有編程經驗更好。:)
    為幫助你理解和操作,我將使用我遇到過的真實事例來演示使用gdb調試有缺陷(bug)的程序過程,你看過這篇筆記后能自己動手練一下最好。

    二、程序中常見的缺陷(bug)分類
    程序(編譯型程序,perl、python,php等腳本程序除外)中常見的bug通常分為兩類: 語法錯誤和邏輯錯誤,或者編譯時錯誤和運行是錯誤。
    語法錯誤(編譯時錯誤)是我們在編寫源代碼時沒有按照相關的語言規范(如ANSI C標準)導致編譯時出錯,編譯失敗。這種錯誤的檢查和調試一般是比較簡單和直接的:因為編譯器(如gcc)通常會明確告訴你錯誤的原因和大致的范圍(注意不一定是準確的錯誤行)。例如下面的一個簡單demo.c程序的第8行缺失了一個分號,gcc指示第10行前少了一個分號。這就是一個典型的語法錯誤。
    geekard@geekard:~/test$ cat -n demo.c 
         1    #include<stdio.h>
         2    
         3    int
         4    main(){
         5    
         6        int n;
         7    
         8        printf("the n is:%c", n)
         9        
        10        return 0;
        11    }
    geekard@geekard:~/test$ gcc demo.c -o demo
    demo.c: In function ‘main’:demo.c:10:
    error: expected ‘;’ before ‘return’
    添加了分號再編譯一次,這下沒有出現問題,運行程序的結果如下:
    geekard@geekard:~/test$ ./demo 
    the n is:6680564 
    另外注意這個程序中的變量n,我定義其為整型變量但并沒有對其初始化賦值,這就是一個邏輯錯誤:編譯器不會指示這個錯誤,只有在實際運行或測試時才能發現。
    這個小程序只是一個故意的編造,但在實際編程中無論你多高明,經驗多豐富,難免會在此處犯些小錯誤(想想吧:當你需要編寫或維護一個成千上萬行的代碼,這種小概率事件就是確定事件了,:)),而通常這些錯誤又是那么的淺顯而易于消除,但是手工“除蟲”(debug),往往是效率低下且讓人厭煩的,本文將就"段錯誤"這個內存訪問越界的錯誤談談如何使用gdb快速定位這些"段錯誤"的語句。

    三、程序調試器(如gdb)有什么用?(參考自gdb的在線幫助手冊, 可用命令:man gdb, 或 info gdb查看)

    程序調試器(如gdb)的主要目的是讓你能夠查看正在執行的程序其內部特性(如執行流程、變量值、函數調用、堆棧等),也可以程序崩潰時刻或以前都發生了什么。
    Gdb對程序的調試能力主要體現在以下四個方面(當然不止這些):   
    . 啟動你的程序,可以帶任何影響其功能(或稱行為)的參數。   
    . 能夠使你的程序在指定條件下在指定的地方(斷點)停止運行。    
    . 當你的程序在斷點處停止時,你可以查看已執行的結果(如變量的值,函數之間的調用情況,執行到那一行代碼,下一步該執行哪行代碼)    
    . 改變你的程序中,你可以實驗這種改變所帶來的影響(如bug消除了,或者情況變得更糟糕)

    使用gdb,你可以調試C,C++,以及Modula-2語言編寫的程序。

    四、段錯誤(Segmental fault)介紹
    在用C/C++語言寫程序的時侯,內存管理的絕大部分工作都是需要我們來做的。實際上,內存管理是一個比較繁瑣的工作,所以像java和c#等語言采用了內存自動回收機制,避免了內存泄漏。如果程序試圖往內存地址0處寫東西時,內核就會向其發送段錯誤信號,如果程序沒有捕獲該信號,默認的操作時內核終止該程序的運行,例如我寫的一個myls程序就遭遇了這種情況:
    luck@geekard:~/codes/12.21$ ./myls -ld .
    longlist 1, typelist 0, dirlist 1, filename .
    Segmentation fault
    luck@geekard:~/codes/12.21$ 

    常見的段錯誤原因如下:
    1)往受到系統保護的內存地址寫數據有些內存是內核占用的或者是其他程序正在使用,為了保證系統正常工作,所以會受到系統的保護,而不能任意訪問
    .2)內存越界(數組越界,變量類型不一致等)
    下面我以上面的myls程序出現的錯誤為例介紹用gdb進行調試的方法和過程。

    五、gdb調試入門

      5.1 調試前的準備
    我們首先要啟動linux內核提供核心轉儲(core dump)機制:當程序中出現內存操作錯誤時,會發生崩潰并產生核心文件(core文件)。使用GDB可以對產生的核心文件進行分析,找出程序是在什么時候崩潰的和在崩潰之前程序都做了些什么。 
    首先,你的Segmentation Fault錯誤必須要能重現(廢話…)。
    然后,依參照下面的步驟來操作:
    1)無論你是用Makefile來編譯,還是直接在命令行手工輸入命令來編譯,都應該加上 -g 選項。如:
    luck@geekard:~/codes/12.21$ ls
    myls-0.0.c  myls-1.0.c  myls-2.0.c
    luck@geekard:~/codes/12.21$ gcc -g -o myls myls-0.0.c 
    luck@geekard:~/codes/12.21$ ls
    myls  myls-0.0.c  myls-1.0.c  myls-2.0.c
    加了-g選項后,gcc就會在生成的可執行文件(這里-o myls表示輸出(output)的可執行文件名時myls)里添加一些調試符號(debugging symbols),有了這些調試符號后就可以在稍后用gdb調試時列出執行的程序的C源代碼了。-g選項增大了文件體積,一般只是在剛開發出的程序調試時使用,當確定無誤編譯出實際使用的可執行文件時就不需要-g選項了。
    2)一般來說,在默認情況下,在程序崩潰時,core文件是不生成的(很多Linux發行版在默認時禁止生成核心文件)。所以,你必須修改這個默認選項,在命令行執行:
    ulimit -c unlimited     //unlimited 表示不限制生成的core文件的大小。
    3)運行你的程序,不管用什么方法,使之重現Segmentation Fault錯誤。
    luck@geekard:~/codes/12.21$ ./myls -ld .
    longlist 1, typelist 0, dirlist 1, filename .
    Segmentation fault (core dumped)
    4)這時,你會發現在你程序同一目錄下,生成了一個文件名為 core的文件,即核心文件。
    luck@geekard:~/codes/12.21$ ls
    core  myls  myls-0.0.c  myls-1.0.c  myls-2.0.cluck@geekard:~/codes/12.21$ 
    5)用GDB調試它,在命令行執行:
    luck@geekard:~/codes/12.21$ gdb ./myls   或者先啟動gdb然后在gdb命令提示符中輸入這兩個文件:

    luck@geekard:~/codes/12.21$ gdb  //不帶參數啟動gdb調試程序
    GNU gdb (GDB) 7.2-ubuntu
    Copyright (C) 2010 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
    and "show warranty" for details.
    This GDB was configured as "i686-linux-gnu".
    For bug reporting instructions, please see:
    <http://www.gnu.org/software/gdb/bugs/>.
    (gdb) file ./myls                    //輸入file命令和你的可執行文件名和路徑,這里為當前目錄下的myls文件
    Reading symbols from /home/luck/codes/12.21/myls...done.
    (gdb) run -ld ./                       //帶參數(這里為 -ld ./)運行r(run)程序,這和在bash命令行上執行:./myls -ld ./效果時一致的。
    Starting program: /home/luck/codes/12.21/myls -ld ./
    longlist 1, typelist 0, dirlist 1, filename ./            //myls程序的輸出

    Program received signal SIGSEGV, Segmentation fault.       //出錯后退出
    0x0016e78f in vfprintf () from /lib/libc.so.6
    (gdb) 
    從這里我們還發現進程是由于收到了SIGSEGV信號而結束的。通過進一步的查閱文檔(man 7 signal),我們知道SIGSEGV默認handler的動作是打印”段錯誤"的出錯信息,并產生Core文件。
    查看一下我的當前目錄,果然有core文件。

    luck@geekard:~/codes/12.21$ ls
    core  myls  myls-0.0.c  myls-1.0.c  myls-2.0.c

    下面我們就用剛才生成的分段錯誤產生的核心轉儲文件(core)再次調試程序。接著上一步的(gdb) 提示符,輸入以下命令:

    (gdb) core core       //輸入core命令和分段錯誤產生的核心轉儲文件,這里為當前目錄下的core文件
    A program is being debugged already.  Kill it? (y or n) y   //按y,重新調試
    [New Thread 24884]
    warning: Can't read pathname for load map: Input/output error.
    Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
    Loaded symbols for /lib/libc.so.6
    Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
    Loaded symbols for /lib/ld-linux.so.2

    Core was generated by `./myls -ld .'.              //core文件記錄了發生錯誤的程序執行的命令行參數
    Program terminated with signal 11, Segmentation fault.
    #0  0x002bb78f in vfprintf () from /lib/libc.so.6  //core文件記錄了發生錯誤時程序的退出狀態
    (gdb) 

    從標號為0的行我們并不能看出程序到底在哪出錯,所以下一步需要確定發生錯誤前程序中函數之間的調用關系
    (gdb) backtrace    //顯示程序的堆棧信息
    #0  0x0014f78f in vfprintf () from /lib/libc.so.6
    #1  0x0016f4dc in vsprintf () from /lib/libc.so.6
    #2  0x00157b4b in sprintf () from /lib/libc.so.6
    #3  0x08048c56 in finalprt (file=0x8a9b02b "..", dirlist=1, typelist=0, 
        longlist=1) at myls-0.0.c:261
    #4  0x080487c3 in detailList (file=0xbfab684d ".", dirlist=1, typelist=0, 
        longlist=1) at myls-0.0.c:132
    #5  0x08048712 in main (argc=3, argv=0xbfab4804) at myls-0.0.c:89
    (gdb) 

    可以看出myls程序的函數調用關系為:
    main() ---> detailList() ---> finalprt 
    然后在標號為0-2的行進入了系統的C庫函數,所以產生錯誤的可能在標號3、4、5指明的函數中。
    我們先看一下最后調用finalprt()函數時可能發生錯誤的代碼行:

    (gdb) frame 3    //上面以#開頭的行稱為幀(frame),這里指定查看第3幀
    #3  0x08048c56 in finalprt (file=0x8a9b02b "..", dirlist=1, typelist=0, 
        longlist=1) at myls-0.0.c:261
    261                    sprintf(str, "%c%d    %d,%d  %d  %d  %s", filetype, permission, uid, gid, size, mdate, file);

    可以看到在調用sprintf()函數時可能發生了分段錯誤(由非法引用內存引起),而sprintf()的原型為: int sprintf(char *str, const char *format, ...);
    最有可能引起錯誤的地方是其第一個參數:char *str,一個指向字符串數組的指針,我們先把疑點放在這,接下來看一下函數之間相互調用時傳遞的參數值和函數的內部變量值:
    (gdb) backtrace  full  //full參數表示完全顯示函數之間相互調用時傳遞的參數值和函數的內部變量值
    #0  0x0014f78f in vfprintf () from /lib/libc.so.6
    No symbol table info available.
    #1  0x0016f4dc in vsprintf () from /lib/libc.so.6
    No symbol table info available.
    #2  0x00157b4b in sprintf () from /lib/libc.so.6
    No symbol table info available.
    #3  0x08048c56 in finalprt (file=0x8a9b02b "..", dirlist=1, typelist=0, 
        longlist=1) at myls-0.0.c:261
            str = 0x4d11faec <Address 0x4d11faec out of bounds>
            flag = 65
    #4  0x080487c3 in detailList (file=0xbfab684d ".", dirlist=1, typelist=0, 
        longlist=1) at myls-0.0.c:132
            ptr = 0x8048e44 "longlist %d, typelist %d, dirlist %d, filename %s\n"
            dirp = 0x8a9b008
            direntp = 0x8a9b020
    #5  0x08048712 in main (argc=3, argv=0xbfab4804) at myls-0.0.c:89
            file = 0xbfab684d "."
            ptr = 0x8048d30 "U\211\345WVS\350O"
            i = 3
            j = 3
            longlist = 1
            dirlist = 1
            typelist = 0
    請注意序號3中的內部變量str的值 <Address 0x4d11faec out of bounds>,這表示發生了數組越界,難怪發生了段錯誤!
    現在我們找到原因了:finalprt()中的第261行調用函數sprintf()時向其傳遞的第一個參數str發生里越界存取,于是內核終止程序的運行。

    下面我們要驗證這個判斷:在261處設置一個斷點,程序運行到斷點后單步執行,觀察是否會發生錯誤。

    (gdb) stop                    //停止當前調試
    (gdb) break 261              //在第261行設置一個斷點
    Breakpoint 1 at 0x8048bf1: file myls-0.0.c, line 261.
    (gdb) run  -ld ./           //帶參數運行程序(myls)
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y   //當然yes

    Starting program: /home/luck/codes/12.21/myls -ld ./
    longlist 1, typelist 0, dirlist 1, filename ./

    Breakpoint 1, finalprt (file=0x804c02b "..", dirlist=1, typelist=0, longlist=1)   //可以看到程序在第261行停止
        at myls-0.0.c:261
    261                    sprintf(str, "%c%d    %d,%d  %d  %d  %s", filetype, permission, uid, gid, size, mdate, file);

    (gdb) where                                //顯示目前函數之間的調用情況與breaktrace命令功能相似
    #0  finalprt (file=0x804c02b "..", dirlist=1, typelist=0, longlist=1)
        at myls-0.0.c:261
    #1  0x080487c3 in detailList (file=0xbffff830 "./", dirlist=1, typelist=0, 
        longlist=1) at myls-0.0.c:132
    #2  0x08048712 in main (argc=3, argv=0xbffff6e4) at myls-0.0.c:89
    (gdb) printf "%d\n",filetype             //打印處函數中的變量filetype的值
    100
    (gdb) list                                //列出斷點處前后的相關代碼
    256    //            if(filetype == 'd')
    257                    sprintf(str, "%s\n", file);
    258                break;
    259            case 0101:
    260    //            if(filetype == 'd')
    261                    sprintf(str, "%c%d    %d,%d  %d  %d  %s", filetype, permission, uid, gid, size, mdate, file);
    262                break;
    263            case 0110:
    264    //            if(filetype == 'd')
    265                    sprintf(str, "%s%c", file, filetype);
    (gdb) n                      //然后單步執行代碼,立即發生了錯誤

    Program received signal SIGSEGV, Segmentation fault.
    0x0016e78f in vfprintf () from /lib/libc.so.6

    可見在線調試驗證了我們的假設,的確時261行的sprintf語句有問題,下面我們看一下261所在的函數finalprt()中變量str的類型
    (gdb) list finalprt    //列出函數finalprt()入口附近的源代碼
    225        *mdate_s = fstat.st_mtime;
    226        return 0;
    227    }
    228    
    229    /*this function prints all the information*/
    230    static char *finalprt(char *file, int dirlist, int typelist, int longlist){
    231    
    232        char *str;
    233        int flag = 0000;
    234        
    (gdb) 
    注意第232行的變量定義:str被錯誤的定義個指向char的指針,而sprintf()的第一個參數要求為一字符型數組的首地址,所以sprintf()調用時會發生內存越界的錯誤。

    接著考慮下去,以前用windows系統下的ie的時侯,有時打開某些網頁,會出現“運行時錯誤”,這個時侯如果恰好你的機器上又裝有windows的編譯器的話,他會彈出來一個對話框,問你是否進行調試,如果你選擇是,編譯器將被打開,并進入調試狀態,開始調試。
    Linux下如何做到這些呢?
    我們可以在要調試的程序中定義一個分段錯誤信號(SIGSEGV)的處理函數(handler),在該函數中中調用gdb,這樣當段錯誤發生時程序就會自動啟動gdb進行調試,一個簡單的示例代碼如下:

    /**
    *段錯誤時啟動調試
    */
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <string.h>

    void
    dump(int signo){

            char buf[1024];
            char cmd[1024];
            FILE *fh;

            snprintf(buf, sizeof(buf), "/proc/%d/cmdline", getpid());  //取得進程的命令行文件地址
            if(!(fh = fopen(buf, "r")))    //打開該文件
                    exit(0);
            if(!fgets(buf, sizeof(buf), fh)) //將其內容讀到buf數組中
                    exit(0);
            fclose(fh);
            if(buf[strlen(buf) - 1] == '\n') //刪除獨到的字符串中最后的還行符并保證字符串以空字符結尾
                    buf[strlen(buf) - 1] = '\0';
            snprintf(cmd, sizeof(cmd), "gdb %s %d", buf, getpid());  //合并命令行參數
            system(cmd);               //執行cmd字符竄 代表的命令

            exit(0);
    }

    void
    dummy_function (void){       //測試函數

            unsigned char *ptr = 0x00;
            *ptr = 0x00;        //向內存中0x00地址寫數據,產生段錯誤
    }

    int
    main (void)
    {
            signal(SIGSEGV, &dump);  //捕獲信號SIGSEGV,當接收到內核發送的SIGSEGV信號時調用處理函數dump()
            dummy_function ();

            return 0;
    }
    編譯運行效果如下:
    luck@geekard test $ gcc -g -rdynamic f.c
    luck@geekard test $ ./a.out
    GNU gdb 6.5
    Copyright (C) 2006 Free Software Foundation, Inc.
    。。。。省略。。。。
    0xffffe410 in __kernel_vsyscall ()
    (gdb) bt
    #0  0xffffe410 in __kernel_vsyscall ()
    #1  0xb7ee4b53 in waitpid () from /lib/libc.so.6
    #2  0xb7e925c9 in strtold_l () from /lib/libc.so.6
    #3  0x08048830 in dump (signo=11) at f.c:22         
    #4  <signal handler called>
    #5  0x0804884c in dummy_function () at f.c:31
    #6  0x08048886 in main () at f.c:38
    第3個frame指示發生錯誤的行為f.c中的22行,即為*ptr = 0x00;行。

    好了,以上就是這篇筆記的主要內容,下面總結一下gdb的主要命令:

    ulimit -c unlimited                                                //打開內核的核心轉儲機制
    gcc -g -o outPutName sourceCodeName.c  //編譯時加-g選項,使生成的可執行文件中包含調試信息
    gdb outPutName core                                       //啟動gdb,可以咋命令行上指定要調試程序
    or:  gdb  file  outPutName                                //也可以在gdb命令提示符中輸入要調試的程序名                           
    core  core                                                           //指定程序執行錯誤時內核生成的轉儲文件
    list  [function]|[row-number]                            //查看源代碼,可以跟函數名或行號
    break [function]|[row-number]                        //設置斷點,可以跟函數名或行號
    clear [function]|[row-number]                         //清除斷點,可以跟函數名或行號或斷點號 
    r     [paramiters]                                                /

    RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
    最近免费观看高清韩国日本大全