C&C++——段錯誤(Segmentation fault)


C/C++中的段錯誤(Segmentation fault)php


Segment fault 之因此可以流行於世,是與Glibc庫中基本全部的函數都默認型參指針爲非空有着密切關係的。
來自:http://oss.lzu.edu.cn/blog/article.php?uid_7/tid_700.html#commenthtml

背景
最近一段時間在linux下用C作一些學習和開發,可是因爲經驗不足,問題多多。而段錯誤就是讓我很是頭痛的一個問題。不過,目前寫一個一千行左右的代碼,也不多出現段錯誤,或者是即便出現了,也很容易找出來,而且處理掉。linux

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

一方面爲了給本身的學習作個總結,另外一方面因爲至今沒有找到一個比較全面介紹這個雖然是「FREQUENTLY ASKED QUESTIONS」的問題,因此我來作個拋磚引玉吧。下面就從上面的幾個問題出發來探討一下「Segmentation faults"吧。sql

目錄
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.ubuntu

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

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

相關文章
相關標籤/搜索