記憶體區段錯誤

記憶體區段錯誤（Segmentation fault，經常被縮寫為 segfault），又譯為記憶體段錯誤，也稱存取許可權衝突（access violation），是一種程式錯誤。

它會出現在當程式企圖存取 CPU 無法定址的記憶體區段時。當錯誤發生時，硬體會通知作業系統產生了記憶體存取許可權衝突的狀況。作業系統通常會產生核心轉儲（core dump）以方便程式員進行除錯。通常該錯誤是由於呼叫一個地址，而該地址為空（NULL）所造成的，例如連結串列中呼叫一個未分配地址的空鏈錶單元的元素。陣列訪問越界也可能產生這個錯誤。

概述

當程式試圖訪問不允許訪問的記憶體位置，或試圖以不允許的方式訪問記憶體位置（例如，嘗試寫入只讀位置，或覆蓋作業系統的一部分）時，會產生儲存器段錯誤。

術語 “分段” 在計算中有多種用途；“儲存器段錯誤” 是自 1950 年代以來就一直使用的術語。當有記憶體保護時，只有程式自己的地址空間是可讀的，其中只有堆疊和程式資料段的可讀寫部分是可寫的，而只讀資料和程式碼段是不可寫的。因此，嘗試讀取程式地址空間之外的資料或寫入至只讀記憶體段時，會導致儲存器段錯誤。

在使用硬體記憶體分段來提供虛擬記憶體的系統上，當硬體檢測到嘗試引用不存在的段、或引用段界限外的記憶體或引用無訪問許可權的記憶體段中的資料時，會發生儲存器段錯誤。在僅使用記憶體分頁的系統上，無效記憶體頁錯誤通常會導致儲存器段錯誤，而儲存器段錯誤和記憶體頁錯誤都是虛擬記憶體管理系統引發的錯誤。儲存器段錯誤也可以獨立於記憶體頁錯誤發生：非法訪問有效的記憶體頁是會導致儲存器段錯誤，而非無效記憶體頁錯誤。並且段錯誤可能發生在記憶體頁中間（因此沒有記憶體頁錯誤），例如處於同一記憶體頁內但非法覆蓋記憶體的緩衝區溢位。

在硬體級別，在非法訪問時，如果引用的記憶體存在，錯誤最初由記憶體管理單元（MMU）丟擲，作為其記憶體保護功能的一部分，或無效記憶體頁錯誤（如果引用的記憶體不存在）。如果問題不是無效的邏輯地址而是無效的物理地址，則會引發匯流排錯誤，儘管並不總是能夠區分這些錯誤。

在作業系統級別，這個錯誤會被捕獲，並傳遞一個訊號給有問題的程序，啟用該程序的訊號處理程式。不同的作業系統有不同的訊號名稱來表示發生了儲存器段錯誤。在類 Unix 作業系統上，一個被稱為 SIGSEGV 的訊號被髮送到該程序。在 Microsoft Windows 上，該程序會收到 STATUSACCESSVIOLATION 異常。

錯誤原因

儲存器段錯誤產生的條件和表現方式取決於硬體和作業系統：不同的硬體會在產生不一樣的錯誤，且不同的作業系統會將這些錯誤轉換成不同的訊號傳送給執行緒。

確定儲存器段錯誤的根本原因在某些情況下十分容易（例如：訪問空指標所指向的記憶體空間），程式會不斷導致儲存器段錯誤。在其他的一些情況，儲存器段錯誤可能難以重現或者在意料不到的時候出現，這會讓尋找儲存器段錯誤的根本原因變得困難。

以下是一些導致儲存器段錯誤的一般原因：

試圖訪問不存在的記憶體空間（程序記憶體空間以外）
試圖訪問沒有許可權的記憶體空間（例如：訪問作業系統核心的記憶體地址）
試圖寫入至只讀記憶體段（例如：程式碼段）

以下是一些導致儲存器段錯誤的一般程式設計錯誤：

引用空指標
引用未初始化的野指標
引用已經被呼叫 free 函式釋放了的懸空指標
緩衝區溢位
堆疊溢位
執行未正確編譯的程式（儘管存在編譯時錯誤，某些編譯器依然會輸出可執行檔案）

在 C 程式碼中，由於容易錯誤地使用指標，儲存器段錯誤最常發生。尤其是在 C 的動態記憶體分配中。

試圖訪問空指標所指向的記憶體區域總是會導致儲存器段錯誤。而野指標和懸空指標則有時會導致儲存器段錯誤，有時則不會。這是因為野指標和懸空指標所指向的記憶體可能存在也可能不存在，可能可寫入也可能不可寫入。這會導致儲存器段錯誤會出現在意料不到的時候。

char *p1 = NULL; // 空指標

char *p2; // 野指標：未被初始化的指標

char p3 = malloc (10 sizeof (char)); // 獲取動態記憶體並初始化指標 (假設 malloc 函式沒有出錯)

free (p3); //p3 所指向的動態記憶體被釋放掉，p3 變成懸空指標

char c1 = *p1; // 試圖訪問空指標所指向的記憶體總是會導致儲存器段錯誤

char c2 = *p2; // 試圖訪問野指標所指向的記憶體會導致隨機資料

char c3 = *p3; // 試圖訪問懸空指標所指向的記憶體可能會導致隨機資料

試圖訪問這些指標中的任何一個所指向的記憶體都可能導致分段錯誤：試圖訪問空指標通常會導致儲存器段錯誤；訪問野指標所指向的記憶體可能會導致隨機資料，因為指標未被初始化，指標所指向的記憶體地址是隨機數；而訪問懸空指標所指向的記憶體可能會在一定時間內訪問到有效資料，但是當該資料被覆蓋掉之後會導致隨機資料。

處理儲存器段錯誤

儲存器段錯誤或匯流排錯誤的預設操作是異常終止觸發該錯誤的程序。可能會生成核心檔案以幫助除錯，並且還可能執行依賴於其他平臺的操作。例如，使用 grsecurity 補丁的 Linux 系統可能會記錄 SIGSEGV 訊號（當發生儲存器段錯誤時，Linux 系統會產生一個 SIGSEGV 訊號，發生該錯誤的程序會捕獲到該訊號並異常終止該程序或者呼叫該程序與該訊號繫結函式），以便監視可能使用緩衝區溢位來的非法入侵。

在某些系統上，例如 Linux 和 Windows，程式本身可以處理儲存器段錯誤。根據體系結構和作業系統的不同，正在執行的程式不僅可以處理事件，還可以提取一些有關其狀態的資訊，例如獲取堆疊跟蹤、處理器暫存器值、觸發時的原始碼行、無效訪問的記憶體地址，以及該操作是讀取還是寫入。

儘管儲存器段錯誤通常意味著程式存在需要修復的錯誤，但也可能出於測試、除錯以及模擬需要直接訪問記憶體的平臺的目的而故意導致此類故障。在後一種情況下，系統必須能夠允許程式在發生故障後繼續執行。在這種情況下，當系統允許時，可以處理該事件並增加處理器程式計數器以 “跳過” 錯誤的指令以繼續執行。

例子

4.1 寫入至只讀記憶體段

試圖寫入至只讀記憶體段會引發儲存器段錯誤。在程式碼錯誤的級別，當程式將資料寫入至其程式碼段的或只讀資料段時，就會發生儲存器段錯誤。

以下是一個 ANSI C 程式碼示例，此段程式碼通常會在具有記憶體保護的平臺上導致儲存器段錯誤。它試圖修改字串文字，根據 ANSI C 標準，這是未定義的行為。大多數編譯器不會在編譯時捕獲它，並將其編譯成會崩潰的可執行程式碼：

int main (void){

const char *s = "hello world\n";

s = 'H';

printf ("% s", s);

return 0;

}

編譯包含此程式碼的程式時，字串 “hello world” 被放置在程式可執行檔案的 rodata 部分：資料段的只讀部分。載入該程式時，作業系統將它與其他字串和常量資料一起放在記憶體的只讀段中。執行時，指標 s 被設定為指向字串的位置，並試圖透過該指標將 H 字元寫入至只讀記憶體段，從而導致儲存器段錯誤。使用編譯時不檢查只讀位置分配的編譯器來編譯這樣的程式，並在類 Unix 作業系統上執行會產生以下執行時錯誤：

$ gcc segfault.c -g -o segfault

$ ./segfault

儲存器段錯誤

GDB 產生的核心檔案：

Program received signal SIGSEGV, Segmentation fault.

0x1c0005c2 in main at segfault.c:6

6 *s = 'H';

可以使用字元陣列來代替字元指標以更正程式碼，因為該字串會儲存在堆疊中，而非只讀資料段中:

int main (void){

char s = "hello world\n";

s = 'H';

printf ("% s", s);

return 0;

}

編譯並執行以上程式碼：

$ gcc nosegfault.c -g -o nosegfault

$ ./segfault

Hello world

儘管不應該修改字串中的文字（這在 C 標準中具有未定義的行為），但在 C 中它們是 static char 型別，因此原始程式碼中沒有隱性轉換（指向該陣列的字元指標），而在 C++ 中，它們是 static const char 型別，存在隱性轉換，因此編譯器通常會捕獲該錯誤。

4.2 試圖訪問空指標所指向的記憶體

在 C 和類 C 語言中，空指標用於表示 “沒有物件的指標” 並作為錯誤指示符，而試圖讀取或寫入空指標所指向的記憶體是非常常見的程式錯誤。C 標準並沒有指明空指標與指向記憶體地址 0 的指標相同，儘管在實踐中可能是這種情況。大多數作業系統把空指標對映至會產生儲存器段錯誤的記憶體。C 標準不保證此行為。試圖讀取或寫入空指標所指向的記憶體在 C 標準中是未定義的行為。

以下示例程式碼建立一個空指標，然後試圖訪問其值（讀取該值）。執行這段程式碼會導致多數作業系統產生儲存器段錯誤：

int *p_num = NULL;

printf ("% d\n", *p_num);

以下示例程式碼建立一個空指標，並試圖寫入資料。執行這段程式碼會導致儲存器段錯誤：

int *p_num = NULL;

p_num = 1;

以下的程式碼包含一個空指標的解引用，但編譯時通常不會導致儲存器段錯誤，因為該值未被使用，因此該解引用通常會被當做死程式碼被消除掉以最佳化程式碼：

int *p_num = NULL;

p_num;

4.3 緩衝區溢位

4.4 堆疊溢位

以下程式碼是一個沒有出口的遞迴：

int main (void){main ;

return 0;

}

這會導致堆疊溢位，從而導致儲存器段錯誤。根據程式語言、編譯器執行的最佳化和程式碼的確切結構，無限遞迴不一定會導致堆疊溢位。在這種情況下，無法訪問程式碼（return 語句）的行為是未定義的，因此編譯器可以消除它並且使用可能導致不用堆疊的尾調最佳化。其他最佳化可能包括將遞迴轉換為迭代，鑑於示例函式的結構，程式將永遠執行下去，同時大機率不會導致堆疊溢位。