コンピューティングにおいて、セグメンテーション違反(しばしばセグフォールトと略される)またはアクセス違反は、メモリ保護を備えたハードウェアによって発生する障害状態であり、ソフトウェアがメモリの制限された領域にアクセスしようとした(メモリアクセス違反)ことをオペレーティングシステム(OS)に通知します。標準的なx86コンピュータでは、これは一般的な保護違反の一種です。オペレーティングシステムカーネルは、これに応じて、通常は何らかの修正措置を実行し、一般的には、プロセスにシグナルを送信することによって、障害を問題のあるプロセスに渡します。プロセスは、場合によってはカスタムシグナルハンドラをインストールして、自分で回復することができますが[ 1 ]、そうでない場合は、OSのデフォルトのシグナルハンドラが使用され、一般的にはプロセスの異常終了(プログラムクラッシュ)を引き起こし、場合によってはコアダンプが発生します。
セグメンテーション違反は、ヌル値を取ることができるポインタ、任意の値に設定できるポインタ、配列をサポートする言語で記述されたプログラムでよく発生するエラーの一種で、メモリチェックがほとんど、あるいは全く行われない場合に発生します。これは主に、仮想メモリのアドレス指定にポインタを使用する際のエラーが原因です。もう1つのメモリアクセスエラーはバスエラーで、これも原因は様々ですが、今日でははるかに稀です。これは主に、物理メモリのアドレス指定の誤り、またはメモリアクセスのアライメントミスが原因で発生します。つまり、プロセスがアクセスを許可されていない参照ではなく、ハードウェアがアドレス指定できないメモリ参照です。
多くのプログラミング言語には、プログラマがプログラム内でセグメンテーション違反を回避し、メモリ保護機能を備えたハードウェアのチェックを改善するために設計されたメカニズムがあります。たとえば、Rust は所有権ベースの[ 2 ]モデルを採用して、メモリの安全性のチェックを確実にしようとしています。[ 3 ] LispやJavaなどの他の言語は、ガベージ コレクション[ 4 ]を採用しており、セグメンテーション違反につながる可能性のある特定の種類のメモリ エラーを回避しています。[ 5 ]



セグメンテーション違反は、プログラムがアクセスを許可されていないメモリ位置にアクセスしようとした場合、または許可されていない方法でメモリ位置にアクセスしようとした場合(たとえば、読み取り専用の場所に書き込もうとしたり、オペレーティングシステムの一部を上書きしようとしたりした場合)に発生します。
コンピューティングにおいて「セグメンテーション」という用語はさまざまな用途で使われますが、「セグメンテーション違反」の文脈では、プログラムのアドレス空間を指します。[ 6 ]メモリ保護により、プログラム自身のアドレス空間のみが読み取り可能となり、そのうちスタックとプログラムのデータセグメントの読み書き可能な部分のみが書き込み可能で、const セグメントとコードセグメントに割り当てられた読み取り専用データは書き込みできません。したがって、プログラムのアドレス空間外を読み取ろうとしたり、アドレス空間の読み取り専用セグメントに書き込もうとしたりすると、プログラムのセグメンテーションが侵害されるため、この名前が付けられています。
ハードウェアメモリセグメンテーションを使用して仮想メモリを提供するシステムでは、存在しないセグメントを参照しようとした場合、セグメントの境界外の場所を参照しようとした場合、またはそのセグメントに付与された権限で許可されていない方法で場所を参照しようとした場合に、ハードウェアがそれを検出してセグメンテーション違反が発生します。ページングのみを使用するシステムでは、無効なページ違反が一般的にセグメンテーション違反につながり、セグメンテーション違反とページ違反はどちらも仮想メモリ管理システムによって発生する違反です。セグメンテーション違反はページ違反とは独立して発生することもあります。有効なページへの不正アクセスはセグメンテーション違反ですが、無効なページ違反ではありません。また、ページの途中でセグメンテーション違反が発生することもあります(したがってページ違反は発生しません)。たとえば、ページ内にとどまるバッファオーバーフローでメモリが不正に上書きされる場合などです。
ハードウェアレベルでは、メモリ管理ユニット(MMU)がメモリ保護機能の一環として、不正アクセス(参照先のメモリが存在する場合)またはページフォルト(参照先のメモリが存在しない場合)を最初に検知してエラーを発生させます。問題が論理アドレスの誤りではなく物理アドレスの誤りである場合は、代わりにバスエラーが発生しますが、これらは必ずしも区別されるとは限りません。
オペレーティングシステムレベルでは、この障害が捕捉され、シグナルが問題のあるプロセスに渡され、そのシグナルのハンドラがアクティブ化されます。セグメンテーション違反が発生したことを示すシグナル名は、オペレーティングシステムによって異なります。Unix系オペレーティングシステムでは、( segmentation violationSIGSEGVの略)と呼ばれるシグナルが問題のあるプロセスに送信されます。Microsoft Windowsでは、問題のあるプロセスは例外を受け取ります。STATUS_ACCESS_VIOLATION
セグメンテーション違反が発生する条件と、その発生の仕方は、ハードウェアとオペレーティングシステムによって異なります。ハードウェアが異なれば、特定の条件下で発生するエラーも異なり、オペレーティングシステムが異なれば、それらをプロセスに渡されるシグナルも異なります。直接的な原因はメモリへのアクセス違反ですが、根本的な原因は一般的に何らかのソフトウェアバグです。プログラムが常にセグメンテーション違反を引き起こす場合(例えば、ヌルポインタの逆参照)は、根本原因の特定(バグのデバッグ)は簡単ですが、バグの再現が難しく、実行ごとにメモリ割り当てに依存する場合(例えば、ダングリングポインタの逆参照)もあります。
セグメンテーション違反の典型的な原因は以下のとおりです。
これらは多くの場合、無効なメモリアクセスを引き起こすプログラミングエラーが原因で発生します。
C言語では、セグメンテーション違反はポインタの使用エラー、特にC言語の動的メモリ割り当てにおけるエラーが原因で発生することが最も多いです。ヌルポインタを逆参照すると未定義の動作が発生し、通常はセグメンテーション違反が発生します。これは、ヌルポインタが有効なメモリ アドレスになり得ないためです。一方、ワイルド ポインタやダングリング ポインタは、存在するかどうかわからないメモリ、読み書き可能かどうかわからないメモリを指しているため、一時的なバグを引き起こす可能性があります。例:
char * p1 = NULL ; // ヌルポインタchar * p2 ; // ワイルドポインタ: まったく初期化されていません。char * p3 = ( char * ) malloc ( 10 * sizeof ( char )); // 割り当てられたメモリへの初期化済みポインタ (malloc が失敗しなかったと仮定)free ( p3 ); // メモリが解放されたため、p3 はダングリングポインタになりますこれらの変数のいずれかを逆参照すると、セグメンテーション違反が発生する可能性があります。ヌルポインタを逆参照すると、通常はセグメンテーション違反が発生しますが、ワイルドポインタから読み取ると、セグメンテーション違反は発生せず、ランダムなデータが取得される可能性があります。また、ダングリングポインタから読み取ると、しばらくの間は有効なデータが得られますが、その後、上書きされてランダムなデータになる可能性があります。
セグメンテーション違反またはバスエラーが発生した場合のデフォルトのアクションは、エラーを引き起こしたプロセスを異常終了することです。デバッグを支援するためにコアファイルが生成されることがあり、プラットフォームに依存するその他のアクションも実行される場合があります。たとえば、 grsecurityパッチを使用しているLinuxシステムでは、バッファオーバーフローを使用した侵入試行の可能性を監視するために、SIGSEGVシグナルがログに記録されることがあります。
LinuxやWindowsなどの一部のシステムでは、プログラム自体がセグメンテーション違反を処理することが可能です。[ 7 ]アーキテクチャとオペレーティングシステムによっては、実行中のプログラムはイベントを処理するだけでなく、スタックトレース、プロセッサレジスタ値、トリガーされたソースコードの行、無効にアクセスされたメモリ アドレス[ 8 ]、アクションが読み取りか書き込みかなど、その状態に関する情報を抽出できる場合があります。[ 9 ]
セグメンテーション違反は一般的にプログラムに修正が必要なバグがあることを意味しますが、テスト、デバッグ、およびメモリへの直接アクセスが必要なプラットフォームのエミュレーションの目的で、意図的にこのような障害を引き起こすことも可能です。後者の場合、システムは障害が発生した後でもプログラムの実行を許可する必要があります。この場合、システムが許可すれば、イベントを処理し、プロセッサプログラムカウンタをインクリメントして、失敗した命令を「ジャンプ」して実行を継続することができます。[ 10 ]

読み取り専用メモリへの書き込みは、セグメンテーション違反を引き起こします。コードエラーのレベルでは、これはプログラムが自身のコードセグメントの一部、またはデータセグメントの読み取り専用部分に書き込んだ場合に発生します。これらの部分は、OSによって読み取り専用メモリにロードされるためです。
以下は、メモリ保護機能を持つプラットフォームでは一般的にセグメンテーション違反を引き起こすANSI Cコードの例です。このコードは文字列リテラルを変更しようとしていますが、これはANSI C規格では未定義動作です。ほとんどのコンパイラはコンパイル時にこれを検出せず、代わりにクラッシュする実行可能コードにコンパイルします。
int main ( void ) { char * s = "hello world" ; * s = 'H' ; }このコードを含むプログラムがコンパイルされると、文字列「hello world」がプログラム実行可能ファイルの読み取り専用セクション(データセグメントの読み取り専用セクション)に配置されます。ロードされると、オペレーティングシステムはそれを他の文字列や定数データとともにメモリの読み取り専用セグメントに配置します。実行されると、変数 が文字列の位置を指すように設定され、変数 を介してH文字をメモリに書き込もうとするため、セグメンテーション違反が発生します。コンパイル時に読み取り専用位置の割り当てをチェックしないコンパイラでこのようなプログラムをコンパイルし、Unix 系オペレーティングシステムで実行すると、次の実行時エラーが発生します。s
$ gcc segfault.c -g -o segfault $ ./segfault セグメンテーション違反プログラムがシグナルSIGSEGV 、セグメンテーション違反を受信しました。0x1c0005c2 in main () at segfault . c : 6 6 * s = 'H' ;このコードは、文字ポインタの代わりに配列を使用することで修正できます。配列を使用すると、スタック上にメモリが割り当てられ、文字列リテラルの値で初期化されます。
char s [] = "hello world" ; s [ 0 ] = 'H' ; // 同等に、*s = 'H';文字列リテラルは変更すべきではない(C 標準では未定義の動作となる)にもかかわらず、C ではstatic char[]型であるため[ 11 ] [ 12 ] [ 13 ]、元のコード(配列を指す)では暗黙の型変換は発生しないがchar*、C++ ではstatic const char[]型であるため暗黙の型変換が発生し、コンパイラは一般的にこの特定のエラーを検出する。
C言語およびC言語に類似した言語では、ヌルポインタは「オブジェクトへのポインタがない」という意味とエラー表示として使用され、ヌルポインタの逆参照(ヌルポインタを介した読み書き)は非常に一般的なプログラムエラーです。C標準では、ヌルポインタがメモリアドレス 0へのポインタと同じであるとは規定されていませんが、実際にはそうなる場合もあります。ほとんどのオペレーティングシステムは、ヌルポインタのアドレスをマッピングし、それにアクセスするとセグメンテーション違反が発生するようにしています。この動作はC標準では保証されていません。ヌルポインタの逆参照はC言語では未定義動作であり、準拠する実装では、逆参照されるポインタはヌルではないと仮定することが認められています。
int * ptr = NULL ; printf ( "%d" , * ptr );このサンプルコードは、ヌルポインタを作成し、その値にアクセスしようとします(値を読み取ろうとします)。多くのオペレーティングシステムでは、この操作によって実行時にセグメンテーション違反が発生します。
ヌルポインタを逆参照してから値を代入する(存在しないターゲットに値を書き込む)と、通常はセグメンテーション違反が発生します。
int * ptr = NULL ; * ptr = 1 ;以下のコードにはヌルポインタの逆参照が含まれていますが、コンパイル時にセグメンテーション違反が発生することはあまりありません。これは、その値が使用されないため、デッドコード削除によって逆参照が最適化され、削除されることが多いためです。
int * ptr = NULL ; * ptr ;以下のコードは、文字配列のs上限を超えてアクセスしています。コンパイラとプロセッサによっては、セグメンテーション違反が発生する可能性があります。
char s [] = "hello world" ; char c = s [ 20 ];もう一つの例は、基本ケースのない再帰です。
int main ( void ) { return main (); }このコードによってスタックがオーバーフローし、セグメンテーション違反が発生します。[ 14 ] 無限再帰は、言語、コンパイラによる最適化、およびコードの正確な構造によっては、必ずしもスタックオーバーフローを引き起こすとは限りません。この場合、到達不能なコード(return文)の動作は未定義であるため、コンパイラはそれを削除し、スタックの使用がなくなる可能性のある末尾呼び出し最適化を使用できます。その他の最適化には、再帰を反復に変換することが含まれます。例の関数の構造を考慮すると、これによりプログラムは永久に実行され、おそらくスタックオーバーフローは発生しません。