一、 什么是“Segmentation fault in Linux”
所謂的段錯誤就是指訪問的內存超過了系統所給這個程序的內存空間,通常這個值是由gdtr來保存的,他是一個48位的寄存器,其中的32位是保存由它指向的gdt表,后13位保存相應於gdt的下標,最后3位包括了程序是否在內存中以及程序的在cpu中的運行級別,指向的gdt是由以64位為一個單位的表,在這張表中就保存着程序運行的代碼段以及數據段的起始地址以及相應的斷限和頁面交換還有程序運行級別和內存粒度等信息,一旦一個程序發生了越界訪問,CPU就會產生相應的異常保護,於是segmentation fault就出現了。
即“當程序試圖訪問不被允許訪問的內存區域(比如,嘗試寫一塊屬於操作系統的內存),或以錯誤的類型訪問內存區域(比如,嘗試寫一塊只讀內存)。這個描述是准確的。為了加深理解,我們再更加詳細的概括一下SIGSEGV。段錯誤應該就是訪問了不可訪問的內存,這個內存要么是不存在的,要么是受系統保護的。
Ø SIGSEGV是在訪問內存時發生的錯誤,它屬於內存管理的范疇
Ø SIGSEGV是一個用戶態的概念,是操作系統在用戶態程序錯誤訪問內存時所做出的處理。
Ø 當用戶態程序訪問(訪問表示讀、寫或執行)不允許訪問的內存時,產生SIGSEGV。
Ø 當用戶態程序以錯誤的方式訪問允許訪問的內存時,產生SIGSEGV。
用戶態程序地址空間,特指程序可以訪問的地址空間范圍。如果廣義的說,一個進程的地址空間應該包括內核空間部分,只是它不能訪問而已。
二、 SIGSEGV產生的可能情況
指針越界和SIGSEGV是最常出現的情況,經常看到有帖子把兩者混淆,而這兩者的關系也確實微妙。在此,我們把指針運算(加減)引起的越界、野指針、空指針都歸為指針越界。SIGSEGV在很多時候是由於指針越界引起的,但並不是所有的指針越界都會引發SIGSEGV。一個越界的指針,如果不引用它,是不會引起SIGSEGV的。而即使引用了一個越界的指針,也不一定引起SIGSEGV。這聽上去讓人發瘋,而實際情況確實如此。SIGSEGV涉及到操作系統、C庫、編譯器、鏈接器各方面的內容,我們以一些具體的例子來說明。
(1)錯誤的訪問類型引起
#include<stdio.h>
#include<stdlib.h>
int main(){
char *c = "hello world";
c[1] = 'H';
}
上述程序編譯沒有問題,但是運行時彈出SIGSEGV。此例中,”hello world”作為一個常量字符串,在編譯后會被放在.rodata節(GCC),最后鏈接生成目標程序時.rodata節會被合並到text segment與代碼段放在一起,故其所處內存區域是只讀的。這就是錯誤的訪問類型引起的SIGSEGV。
(2)訪問了不屬於進程地址空間的內存
#include <stdio.h>
#include <stdlib.h>
int main(){
int* p = (int*)0xC0000fff;
*p = 10;
}
還有一種可能,往受到系統保護的內存地址寫數據,最常見的就是給一個指針以0地址;
int i=0;
scanf ("%d", i); /* should have used &i */
printf ("%d\n", i);
return 0;
(3)訪問了不存在的內存
最常見的情況不外乎解引用空指針了,如:
int *p = null;
*p = 1;
在實際情況中,此例中的空指針可能指向用戶態地址空間,但其所指向的頁面實際不存在。
(4)內存越界,數組越界,變量類型不一致等
#include <stdio.h>
int main(){
char test[1];
printf("%c", test[10]);
return 0;
}
這就是明顯的數組越界了,或者這個地址根本不存在。
(5)試圖把一個整數按照字符串的方式輸出
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輸出或存放起來,如:
#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型按照字符串轉換
(6)棧溢出了,有時SIGSEGV,有時卻啥都沒發生
大部分C語言教材都會告訴你,當從一個函數返回后,該函數棧上的內容會被自動“釋放”。“釋放”給大多數初學者的印象是free(),似乎這塊內存不存在了,於是當他訪問這塊應該不存在的內存時,發現一切都好,便陷入了深深的疑惑。
三、調試定位SIGSEGV
在用C/C++語言寫程序的時侯,內存管理的絕大部分工作都是需要我們來做的。實際上,內存管理是一個比較繁瑣的工作,無論你多高明,經驗多豐富,難免會在此處犯些小錯誤,而通常這些錯誤又是那么的淺顯而易於消除。但是手工“除蟲”(debug),往往是效率低下且讓人厭煩的,使用gdb來快速定位這些”段錯誤”的語句。其實還有很多其他的方法。對於一些大型一點的程序,如何跟蹤並找到程序中的段錯誤位置就是需要掌握的一門技巧拉。
1)在程序內部的關鍵部位輸出(printf)信息,那樣可以跟蹤段錯誤在代碼中可能的位置
為了方便使用這種調試方法,可以用條件編譯指令#ifdef DEBUG和#endif把printf函數給包含起來,編譯的時候加上-DDEBUG參數就可以查看調試信息。反之,不加上該參數進行調試就可以。
2)用gdb來調試,在運行到段錯誤的地方,會自動停下來並顯示出錯的行和行號
這個應該是很常用的,如果需要用gdb調試,記得在編譯的時候加上-g參數,用來顯示調試信息。gcc應該都有安裝的。
首先安裝gdb: sudo aot-get install gdb
下面是對某個小程序的的調試過程截圖:
運行gcc的時候加上-g這個參數查看調試信息,
l:(list)顯示我們的源代碼
b 行號:在相應的行上設置斷點,我在第六行設置
r : run 運行程序至斷點
p:p(print)打印變量的值
n:n(next)執行下一步 出現錯誤信息了
c : continue 繼續執行
quit : 退出gdb
防止Segmentation fault的出現需要注意:
定義了指針以后記得初始化,在使用的時候記得判斷是否為NULL;
在使用數組的時候是否被初始化,數組下標是否越界,數組元素是否存在等;
在變量處理的時候變量的格式控制是否合理等;