记忆段错误

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

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

## 概述

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

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

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

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

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

## 錯誤原因

儲存器段錯誤產生的條件和表現方式取決於硬體和作業系統：不同的硬體會在產生不一樣的錯誤，且不同的作業系統會將這些錯誤轉換成不同的訊號傳送給執行緒。
確定儲存器段錯誤的根本原因在某些情況下十分容易（例如：存取空指標所指向的記憶體空間），程式會不斷導致儲存器段錯誤。在其他的一些情況，儲存器段錯誤可能難以重現或者在意料不到的時候出現，這會讓尋找儲存器段錯誤的根本原因變得困難。

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

* 試圖存取不存在的記憶體空間（行程記憶體空間以外）
* 試圖存取沒有權限的記憶體空間（例如：存取作業系統核心的記憶體位址）
* 試圖寫入至唯讀記憶體段（例如：代碼段）

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

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

在 C 代碼中，由於容易錯誤地使用指標，儲存器段錯誤最常發生。尤其是在 C 的動態記憶體分配中。
試圖存取空指標所指向的記憶體區域總是會導致儲存器段錯誤。而野指標和懸空指標則有時會導致儲存器段錯誤，有時則不會。這是因為野指標和懸空指標所指向的記憶體可能存在也可能不存在，可能可寫入也可能不可寫入。這會導致儲存器段錯誤會出現在意料不到的時候。

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

## 處理儲存器段錯誤

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

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

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

## 例子

### 寫入至唯讀記憶體段

試圖寫入至唯讀記憶體段會引發儲存器段錯誤。在代碼錯誤的級別，當程式將資料寫入至其代碼段的或唯讀資料段時，就會發生儲存器段錯誤。
以下是一個 ANSI C 代碼範例，此段代碼通常會在具有記憶體保護的平台上導致儲存器段錯誤。它試圖修改字串文字，根據 ANSI C 標準，這是未定義的行為。大多數編譯器不會在編譯時擷取它，並將其編譯成會崩潰的可執行代碼：

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

GDB 產生的核心檔案：

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

編譯並執行以上代碼：

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

### 試圖存取空指標所指向的記憶體

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

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

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

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

### 緩衝區溢位

### 堆疊溢位

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

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

## 參考資料