天天看點

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

用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 

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錯誤。

Segmentation fault (core dumped)

4)這時,你會發現在你程式同一目錄下,生成了一個檔案名為 core的檔案,即核心檔案。

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.

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:

(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檔案。

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檔案記錄了發生錯誤時程式的退出狀态

從标号為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

可以看出myls程式的函數調用關系為:

main() ---> detailList() ---> finalprt 

然後在标号為0-2的行進入了系統的C庫函數,是以産生錯誤的可能在标号3、4、5指明的函數中。

我們先看一下最後調用finalprt()函數時可能發生錯誤的代碼行:

(gdb) frame 3    //上面以#開頭的行稱為幀(frame),這裡指定檢視第3幀

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參數表示完全顯示函數之間互相調用時傳遞的參數值和函數的内部變量值

No symbol table info available.

        str = 0x4d11faec <Address 0x4d11faec out of bounds>

        flag = 65

        ptr = 0x8048e44 "longlist %d, typelist %d, dirlist %d, filename %s\n"

        dirp = 0x8a9b008

        direntp = 0x8a9b020

        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

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

(gdb) where                                //顯示目前函數之間的調用情況與breaktrace指令功能相似

#0  finalprt (file=0x804c02b "..", dirlist=1, typelist=0, longlist=1)

#1  0x080487c3 in detailList (file=0xbffff830 "./", dirlist=1, typelist=0, 

#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')

262                break;

263            case 0110:

264    //            if(filetype == 'd')

265                    sprintf(str, "%s%c", file, filetype);

(gdb) n                      //然後單步執行代碼,立即發生了錯誤

Program received signal SIGSEGV, Segmentation fault.

可見線上調試驗證了我們的假設,的确時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        

注意第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數組中

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

}

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]                                                /

繼續閱讀