C/C++中的段錯誤(Segmentation fault)[轉]


Segment fault 之所以能夠流行於世,是與Glibc庫中基本所有的函數都默認型參指針為非空有着密切關系的。

 

來自:http://oss.lzu.edu.cn/blog/article.php?uid_7/tid_700.html#comment

 

背景

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

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

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

目錄

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

正文

1。什么是段錯誤?

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

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

所謂的段錯誤 就是指訪問的內存超出了系統所給這個程序的內存空間,通常這個值是由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)
在主題帖子里頭,作者這么寫道:

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

Segment fault 之所以能夠流行於世,是與Glibc庫中基本所有的函數都默認型參指針為非空有着密切關系的。

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

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

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



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

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

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

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

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

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

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);
   printf("%d\n",i);
   return 0;
}
   編譯和執行一下

$ gcc -o segerr segerr.c
$ ./segerr
10
段錯誤


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

下面我們來調試一下,看看是什么原因?

$ gcc -g -o segerr segerr.c        --加-g選項查看調試信息
$ gdb ./segerr
(gdb) l                    --用l(list)顯示我們的源代碼
1       #i nclude <stdio.h>
2
3       int
4       main()
5       {
6               int i = 0;
7
8               scanf ("%d", 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);  --[試圖往地址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的信息。

$ 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來查看段錯誤所在的行

$ gcc -g -o segerr segerr.c
$ gdb ./segerr
(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的源代碼的這里

聲明部分:
    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>
  int main(){
     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命令
通過查看幫助信息,可以看到

Catch segmentation faults in programs


這個東西就是用來撲獲段錯誤的,它通過動態加載器(ld-linux.so)的預加載機制(PRELOAD)把一個事先寫好的庫(/lib/libSegFault.so)加載上,用於捕捉斷錯誤的出錯信息。

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


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

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

后記

雖然感覺沒有寫什么東西,但是包括查找資料和打字,也花了好些幾個小時,不過總結一下也是值得的,歡迎和我一起交流和討論,也歡迎對文章中表達不當甚至是錯誤的地方指正一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM