linux段錯誤

來源:互聯網
上載者:User

最近一段時間在linux下用C做一些學習和開發,但是由於經驗不足,問題多多。而段錯誤就是讓我非常頭痛的一個問題。不過,目前寫一個一千行左右的代碼,也很少出現段錯誤,或者是即使出現了,也很容易找出來,並且處理掉。

       那什麼是段錯誤?段錯誤為什麼是個麻煩事?以及怎麼發現程式中的段錯誤以及如何避免發生段錯誤呢?

      一方面為了給自己的學習做個總結,另一方面由於至今沒有找到一個比較全面介紹這個雖然是“particular problem”的問題,所以我來做個拋磚引玉吧。下面就從上面的幾個問題出發來探討一下“Segmentation faults"吧。

目錄

1. 什麼是段錯誤?
2. 為什麼段錯誤這麼“麻煩”?
3. 編程中通常碰到段錯誤的地方有哪些?
4. 如何發現程式中的段錯誤並處理掉?

本文

1. 什麼是段錯誤?

下面是來自Answers.com的定義:
Quote:

      A segmentation fault (often shortened to segfault) is a particular error condition that can occur during the operation of computer software. In short, a segmentation fault occurs when a program attempts to access a memory location that it is not allowed to access, or attempts to access a memory location in a way that is not allowed (e.g., attempts to write to a read-only location, or to overwrite part of the operating system). Systems based on processors like the Motorola 68000 tend to refer to these events as Address or Bus errors.

      Segmentation is one approach to memory management and protection in the operating system. It has been superseded by paging for most purposes, but much of the terminology of segmentation is still used, "segmentation fault" being an example. Some operating systems still have segmentation at some logical level although paging is used as the main memory management policy.

      On Unix-like operating systems, a process that accesses invalid memory receives the SIGSEGV signal. On Microsoft Windows, a process that accesses invalid memory receives the STATUS_ACCESS_VIOLATION exception.

另外,這裡有個基本上對照的中文解釋,來自http://www.linux999.org/html_sql/3/132559.htm
Quote:

       所謂的段錯誤就是指訪問的記憶體超出了系統所給這個程式的記憶體空間,通常這個值是由gdtr來儲存的,他是一個48位的寄存器,其中的32位是儲存由它指向的 gdt表,後13位儲存相應於gdt的下標,最後3位包括了程式是否在記憶體中以及程式的在cpu中的運行層級,指向的gdt是由以64位為一個單位的表,在這張表中就儲存著程式啟動並執行程式碼片段以及資料區段的起始地址以及與此相應的段限和頁面交換還有程式運行層級還有記憶體粒度等等的資訊。一旦一個程式發生了越界訪問,cpu就會產生相應的異常保護,於是segmentation fault就出現了

       通過上面的解釋,段錯誤應該就是訪問了不可訪問的記憶體,這個記憶體區要麼是不存在的,要麼是受到系統保護的。

2. 為什麼段錯誤這麼麻煩?

      中國linux論壇有一篇精華文章《Segment fault 之永遠的痛》

(http://www.linuxforum.net/forum/gshowflat.php?Cat=&Board=program&Number=193239&page=2&view=collapsed&sb=5&o=all&fpart=1&vc=1)

在主題文章裡頭,作者這麼寫道:
Quote:

       寫程式好多年了,Segment fault 是許多C程式員頭疼的提示。指標是好東西,但是隨著指標的使用卻誕生了這個同樣威力巨大的惡魔。

       Segment fault 之所以能夠流行於世,是與Glibc庫中基本所有的函數都預設型參指標為非空有著密切關係的。

       不知道什麼時候才可以有能夠處理NULL的glibc庫誕生啊!

        不得已,我現在為好多的函數做了衣服,避免glibc的函數被NULL給感染,導致我的Mem訪問錯誤,而我還不知道NULL這個病毒已經在侵蝕我的身體了。

       Segment fault 永遠的痛......

      後面有好多網友都跟帖了,討論了Segmentation faults為什麼這麼“痛”,尤其是對於伺服器程式來說,是非常頭痛的,為了提高效率,要盡量減少一些不必要的段錯誤的“判斷和處理”,但是不檢查又可能會存在段錯誤的隱患。

      那麼如何處理這個“麻煩”呢?
      就像人不可能“完美”一樣,由人創造的“電腦語言“同樣沒有“完美”的解決辦法。
     我們更好的解決辦法也許是:

      通過學習前人的經驗和開發的工具,不斷的嘗試和研究,找出更恰當的方法來避免、發現並處理它。對於一些常見的地方,我們可以避免,對於一些“隱藏”的地方,我們要發現它,發現以後就要及時處理,避免留下隱患。

     下面我們可以通過具體的實驗來舉出一些經常出現段錯誤的地方,然後再舉例子來發現和找出這類錯誤藏身之處,最後處理掉。

3. 編程中通常碰到段錯誤的地方有哪些?

     為了進行下面的實驗,我們需要準備兩個工具,一個是gcc,一個是gdb,我是在ubuntu下做的實驗,安裝這兩個東西是比較簡單的。
Quote:

      sudo apt-get install gcc-4.0 libc6-dev
      sudo apt-get install gdb

     好了,開始進入我們的實驗,我們粗略的分一下類:

     1)往受到系統保護的記憶體位址寫資料

     有些記憶體是核心佔用的或者是其他程式正在使用,為了保證系統正常工作,所以會受到系統的保護,而不能任意訪問。

例子1: Code:

#include <stdio.h>

     int main(){       

            int i = 0;       

          scanf ("%d", i);              /* should have used &i */       

          printf ("%d\n", i);       

          return 0;

}

    編譯和執行一下
Quote:

falcon@falcon:~/temp$ gcc -o segerr segerr.c
falcon@falcon:~/temp$ ./segerr
10
段錯誤

     咋一看,好像沒有問題哦,不就是讀取一個資料然後給輸出來嗎?

     下面我們來調試一下,看看是什麼原因?
Quote:

falcon@falcon:~/temp$ gcc -g -o segerr segerr.c        --加-g選項查看調試資訊
falcon@falcon:~/temp$ gdb ./segerr
GNU gdb 6.4-debian
Copyright 2005 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...Using host libthread_db library "/ lib/tls/i686/cmov/libthread_db.so.1".

(gdb) l                    --用l(list)顯示我們的原始碼
1       #include <stdio.h>
2
3       int
4       main()
5       {
6               int i = 0;
7
8               scanf ("%d", i); /* should have used &i */
9               printf ("%d\n", i);
10              return 0;
(gdb) b 8                --用b(break)設定斷點
Breakpoint 1 at 0x80483b7: file segerr.c, line 8.
(gdb) p i                --用p(print)列印變數i的值[看到沒,這裡i的值是0哦]
$1 = 0

(gdb) r                    --用r(run)運行,直到斷點處
Starting program: /home/falcon/temp/segerr

Breakpoint 1, main () at segerr.c:8
8               scanf ("%d", i); /* should have used &i */ --[試圖往地址0處寫進一個值]
(gdb) n                    --用n(next)執行下一步
10

Program received signal SIGSEGV, Segmentation fault.
0xb7e9a1ca in _IO_vfscanf () from /lib/tls/i686/cmov/libc.so.6
(gdb) c            --在上面我們接收到了SIGSEGV,然後用c(continue)繼續執行
Continuing.

Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
(gdb) quit        --退出gdb

果然
我們“不小心”把&i寫成了i
而我們剛開始初始化了i為0,這樣我們不是試圖向記憶體位址0存放一個值嗎?實際上很多情況下,你即使沒有初始化為零,預設也可能是0,所以要特別注意。

補充:
可以通過man 7 signal查看SIGSEGV的資訊。
Quote:

falcon@falcon:~/temp$ man 7 signal | grep SEGV
Reformatting signal(7), please wait...
       SIGSEGV      11       Core    Invalid memory reference

例子2:Code:

#include <stdio.h>

int main(){       

      char *p;        p = NULL;        *p = 'x';        printf("%c", *p);        return 0;

}

很容易發現,這個例子也是試圖往記憶體位址0處寫東西。

這裡我們通過gdb來查看段錯誤所在的行
Quote:

falcon@falcon:~/temp$ gcc -g -o segerr segerr.c
falcon@falcon:~/temp$ gdb ./segerr
GNU gdb 6.4-debian
Copyright 2005 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".

(gdb) r        --直接運行,我們看到拋出段錯誤以後,自動顯示出了出現段錯誤的行,這就是一個找出段錯誤的方法
Starting program: /home/falcon/temp/segerr

Program received signal SIGSEGV, Segmentation fault.
0x08048516 in main () at segerr.c:10
10              *p = 'x';
(gdb)

2)記憶體越界(數組越界,變數類型不一致等)

例子3:Code:

#include <stdio.h>

int main(){       

char test[1];        printf("%c", test[1000000000]);        return 0;

}

這裡是比較極端的例子,但是有時候可能是會出現的,是個明顯的數組越界的問題或者是這個地址是根本就不存在的。

例子4:Code:

#include <stdio.h>

int main(){       

    int b = 10;        printf("%s\n", b);        return 0;

}

我們試圖把一個整數按照字串的方式輸出出去,這是什麼問題呢?
由於還不熟悉調試動態連結程式庫,所以我只是找到了printf的原始碼的這裡。

Quote:

聲明部分:
    int pos =0 ,cnt_printed_chars =0 ,i ;
unsigned char *chptr ;
va_list ap ;
%s格式控制部分:
case 's':
    chptr =va_arg (ap ,unsigned char *);
    i =0 ;
    while (chptr [i ])
    {...
        cnt_printed_chars ++;
        putchar (chptr [i ++]);
}

       仔細看看,發現了這樣一個問題,在列印字串的時候,實際上是列印某個地址開始的所有字元,但是當你想把整數當字串列印的時候,這個整數被當成了一個地址,然後printf從這個地址開始去列印字元,指導某個位置上的值為\0。所以,如果這個整數代表的地址不存在或者不可訪問,自然也是訪問了不該訪問的記憶體——segmentation fault。

類似的,還有諸如:sprintf等的格式控制問題。比如,試圖把char型或者是int的按照%s輸出或存放起來,如:Code:

#include <stdio.h>

#include <string.h>

char c='c';int i=10;char buf[100];printf("%s", c);        //試圖把char型按照字串格式輸出,這裡的字元會解釋成整數,再解釋成地址,所以原因同上面那個例子

printf("%s", i);            //試圖把int型按照字串輸出

memset(buf, 0, 100);

sprintf(buf, "%s", c);    //試圖把char型按照字串格式轉換

memset(buf, 0, 100);

sprintf(buf, "%s", i);   //試圖把int型按照字串轉換

3)其他

       其實大概的原因都是一樣的,就是段錯誤的定義。但是更多的容易出錯的地方就要自己不斷積累,不段發現,或者吸納前人已經積累的經驗,並且注意避免再次發生。

例如:

<1> 定義了指標後記得初始化,在使用的時候記得判斷是否為NULL。
<2> 在使用數組的時候是否被初始化,數組下標是否越界,數組元素是否存在等。
<3> 在變數處理的時候變數的格式控制是否合理等。

再舉一個比較不錯的例子:

我在進行一個多線程編程的例子裡頭,定義了一個線程數組

#define THREAD_MAX_NUM
pthread_t thread[THREAD_MAX_NUM];

用pthread_create建立了各個線程,然後用pthread_join來等待線程的結束

      剛開始我就直接等待,在建立線程都成功的時候,pthread_join能夠順利等待各個線程結束,但是一旦建立線程失敗,那用pthread_join來等待那個本不存在的線程時自然會存在訪問不能訪問的記憶體的情況,從而導致段錯誤的發生,後來,通過不斷調試和思考,並且得到網路上資料的協助,找到了上面的原因和解決辦法:

       在建立線程之前,先初始化我們的線程數組,在等待線程的結束的時候,判斷線程是否為我們的初始值
如果是的話,說明我們的線程並沒有建立成功,所以就不能等拉。否則就會存在釋放那些並不存在或者不可訪問的記憶體空間。

       上面給出了很常見的幾種出現段錯誤的地方,這樣在遇到它們的時候就容易避免拉。但是人有時候肯定也會有疏忽的,甚至可能還是會經常出現上面的問題或者其他常見的問題,所以對於一些大型一點的程式,如何跟蹤並找到程式中的段錯誤位置就是需要掌握的一門技巧拉。

4. 如何發現程式中的段錯誤?

有個網友對這個做了比較全面的總結,除了感謝他外,我把地址弄了過來。文章名字叫《段錯誤bug的調試》(http://www.cublog.cn/u/5251/showart.php?id=173718),應該說是很全面的。

而我常用的調試方法有:

1)在程式內部的關鍵部位輸出(printf)資訊,那樣可以跟蹤 段錯誤 在代碼中可能的位置

為了方便使用這種調試方法,可以用條件編譯指令#ifdef DEBUG和#endif把printf函數給包含起來,編譯的時候加上-DDEBUG參數就可以查看調試資訊。反之,不加上該參數進行調試就可以。

2)用gdb來調試,在運行到段錯誤的地方,會自動停下來並顯示出錯的行和行號

這個應該是很常用的,如果需要用gdb調試,記得在編譯的時候加上-g參數,用來顯示調試資訊,對於這個,網友在《段錯誤bug的調試》文章裡創造性的使用這樣的方法,使得我們在執行程式的時候就可以動態撲獲段錯誤可能出現的位置:通過撲獲SIGSEGV訊號來觸發系統調用gdb來輸出調試資訊。如果加上上面提到的條件編譯,那我們就可以非常方便的進行段錯誤的調試拉。

3)還有一個catchsegv命令
通過查看協助資訊,可以看到
Quote:

Catch segmentation faults in programs

這個東西就是用來撲獲段錯誤的,不過列印出來的是一些register裡頭的東西,“看不太懂”。

到這裡,“初級總結篇”算是差不多完成拉。歡迎指出其中表達不當甚至錯誤的地方,先謝過!

參考資料[具體地址在上面的文章中都已經給出拉]:

1. 段錯誤的定義
Ansers.com
http://www.answers.com 
Definition of "Segmentation fault"
http://www.faqs.org/qa/qa-673.html 
2. 《什麼是段錯誤》
http://www.linux999.org/html_sql/3/132559.htm 
3.《Segment fault 之永遠的痛》
http://www.linuxforum.net/forum/gshowflat.php?Cat=&Board=program&Number=193239&page=2&view=collapsed&sb=5&o=all&fpart= 
4.《段錯誤bug的調試》
http://www.cublog.cn/u/5251/showart.php?id=173718

相關文章

Beyond APAC's No.1 Cloud

19.6% IaaS Market Share in Asia Pacific - Gartner IT Service report, 2018

Learn more >

Apsara Conference 2019

The Rise of Data Intelligence, September 25th - 27th, Hangzhou, China

Learn more >

Alibaba Cloud Free Trial

Learn and experience the power of Alibaba Cloud with a free trial worth $300-1200 USD

Learn more >

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。