セグメンテーションフォールトは内部でどのように機能しますか?


266

「CPUのMMUが信号を送信する」と「カーネルが問題のあるプログラムにそれを送り、それを終了する」以外に、これに関する情報を見つけることができないようです。

私はそれがおそらくシグナルをシェルに送信し、シェルが問題のあるプロセスを終了して印刷することによってそれを処理すると仮定しました"Segmentation fault"。そのため、crsh(crap shell)と呼ばれる非常に最小限のシェルを作成して、この仮定をテストしました。このシェルは、ユーザー入力を取得してsystem()メソッドに渡すこと以外は何もしません。

#include <stdio.h>
#include <stdlib.h>

int main(){
    char cmdbuf[1000];
    while (1){
        printf("Crap Shell> ");
        fgets(cmdbuf, 1000, stdin);
        system(cmdbuf);
    }
}

そこで、私はこのシェルを裸の端末でbash実行しました(下で実行せずに)。次に、セグメンテーション違反を生成するプログラムを実行しました。私の仮定が正しければ、これはa)クラッシュcrsh、xtermのクローズ、b)印刷しない"Segmentation fault"、またはc)両方のいずれかです。

braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]

正方形に戻って、私は推測する。これを行うのはシェルではなく、その下のシステムであることを示しました。「セグメンテーション違反」はどのように印刷されますか?「誰」がやっているの?カーネル?他に何か?信号とそのすべての副作用は、ハードウェアからプログラムの最終的な終了までどのように伝播しますか?


43
crshこの種の実験には素晴らしいアイデアです。それとその背後にあるアイデアについて私たち全員に知らせてくれてありがとう。
ブルースエディガー16年

30
初めて見たときcrsh、「クラッシュ」と発音されると思いました。それが同様にふさわしい名前であるかどうかはわかりません。
jpmc26

56
これは素晴らしい実験です...しかし、あなたは内部で何system()が起こるかを知っている必要があります。system()シェルプロセスが生成されることがわかりました!そのため、シェルプロセスは別のシェルプロセスを生成し、そのシェルプロセス(/bin/shまたはそのようなもの)がプログラムを実行します。方法/bin/shまたはbash動作は、fork()and exec()(またはexecve()ファミリ内の別の関数)を使用することです。
ディートリッヒエップ

4
@BradenBest:そのとおりです。マニュアルページを読んでくださいman 2 wait、それはマクロWIFSIGNALED()WTERMSIG()
ディートリッヒエップ

4
@DietrichEppあなたが言ったように!(WIFSIGNALED(status) && WTERMSIG(status) == 11)何かおかしい("YOU DUN GOOFED AND TRIGGERED A SEGFAULT")を印刷するためのチェックを追加してみました。segfault内からプログラムを実行すると、crshまさにそれを出力しました。一方、通常終了するコマンドはエラーメッセージを生成しません。
ブレーデンベスト

回答:


248

最新のすべてのCPUには、現在実行中のマシン命令を中断する能力があります。何も起こらなかったように、後で実行を再開できるように、十分な状態(通常、常にではありませんが、常にスタック)を保存します(中断された命令は通常ゼロから再開されます)。次に、割り込みハンドラの実行を開始します。これは単なるマシンコードですが、CPUが事前にその場所を認識できるように特別な場所に配置されます。割り込みハンドラーは、常にオペレーティングシステムのカーネルの一部です。これは、最大の特権で実行され、他のすべてのコンポーネントの実行を監視するコンポーネントです。1,2

割り込みは同期、つまり、現在実行中の命令が行った何かに対する直接の応答としてCPU自体によってトリガーされることを意味します。または、非同期は、ネットワークに到着するデータなどの外部イベントのために予測できない時間に発生することを意味します港。一部の人々は非同期割り込みに「割り込み」という用語を予約し、代わりに同期割り込みを「トラップ」、「障害」、または「例外」と呼びますが、これらの単語はすべて他の意味を持っているため、「同期割り込み」に固執します。

現在、ほとんどの最新のオペレーティングシステムにはプロセスの概念があります。最も基本的には、これはコンピューターが複数のプログラムを同時に実行できるメカニズムですが、オペレーティングシステムがメモリ保護を構成する方法の重要な側面でもあります。まだすべてではありません)最新のCPU。それは仮想メモリと一緒に行きます、これはメモリアドレスとRAM内の実際の場所の間のマッピングを変更する機能です。メモリ保護により、オペレーティングシステムは各プロセスに専用のRAMチャンクを与え、それだけがアクセスできるようにします。また、オペレーティングシステム(プロセスの代わりに動作する)が、RAMの領域を読み取り専用、実行可能、協調プロセスのグループ間で共有などとして指定することもできます。カーネル。3

CPUが許可するように構成されている方法でのみ各プロセスがメモリにアクセスする限り、メモリ保護は見えません。プロセスがルールに違反すると、CPUは同期割り込みを生成し、カーネルに処理を依頼します。プロセスが実際にルールに違反することはなく、プロセスの続行を許可する前にカーネルのみが何らかの作業を行う必要があることが定期的に発生します。たとえば、RAMのスペースを他の何かのために解放するためにプロセスのメモリのページをスワップファイルに「排除」する必要がある場合、カーネルはそのページにアクセス不可とマークします。プロセスが次にそれを使用しようとすると、CPUはメモリ保護割り込みを生成します。カーネルはスワップからページを取得し、元の場所に戻し、再びアクセス可能としてマークし、実行を再開します。

しかし、プロセスが実際にルールを破ったと仮定します。RAMがマップされたことのないページにアクセスしようとしたか、マシンコードを含まないなどのマークが付けられたページを実行しようとしました。一般に「Unix」として知られるオペレーティングシステムのファミリはすべて、この状況に対処するために信号を使用します4シグナルは割り込みに似ていますが、ハードウェアによって生成され、カーネルによってフィールド化されるのではなく、カーネルによって生成され、プロセスによってフィールド化されます。プロセスはシグナルハンドラを定義できます独自のコードで、それらがどこにあるかをカーネルに伝えます。これらのシグナルハンドラは、必要に応じて実行され、通常の制御フローを中断します。シグナルにはすべて数字と2つの名前があり、1つは不可解な頭字語で、もう1つはややわかりにくいフレーズです。プロセスがメモリ保護ルールに違反したときに生成されるシグナルは(慣例により)番号11であり、その名前はSIGSEGV「セグメンテーション違反」です。5,6

シグナルと割り込みの重要な違いは、すべてのシグナルにデフォルトの動作があることです。オペレーティングシステムがすべての割り込みのハンドラーの定義に失敗すると、それはOSのバグであり、CPUが欠落しているハンドラーを呼び出そうとするとコンピューター全体がクラッシュします。ただし、プロセスには、すべてのシグナルのシグナルハンドラを定義する義務はありません。カーネルがプロセスのシグナルを生成し、そのシグナルがデフォルトの動作のままになっている場合、カーネルは先に進み、デフォルトを実行し、プロセスに影響を与えません。ほとんどのシグナルのデフォルトの動作は、「何もしない」または「このプロセスを終了し、おそらくコアダンプを生成する」です。SIGSEGV後者の1つです。

要約すると、メモリ保護ルールを破ったプロセスがあります。CPUはプロセスを中断し、同期割り込みを生成しました。カーネルはその割り込みをSIGSEGV処理し、プロセスのシグナルを生成しました。プロセスがなかったと仮定しようではないためにシグナルハンドラを設定しSIGSEGV、そのカーネルはプロセスを終了することとなる、デフォルトの動作を行います。これには、_exitシステムコールと同じ効果があります。開いているファイルが閉じられたり、メモリが割り当て解除されたりします。

この時点まで、人間が見ることのできるメッセージは何も印刷されておらず、シェル(より一般的には、終了したばかりのプロセスの親プロセス)はまったく関与していません。親SIGSEGVはなく、ルールを破ったプロセスに行きます。ただし、シーケンスの次のステップは、子プロセスが終了したことを親プロセスに通知することです。これは、親がすでにの一つを使用して、この通知を待っているとき、最も簡単なのとなっているいくつかの異なる方法で起こることができるwaitシステムコール(waitwaitpidwait4、など)。その場合、カーネルはそのシステムコールを返すだけで、終了ステータスと呼ばれるコード番号を親プロセスに提供します7終了ステータスは、子プロセスが終了した理由を親に通知します。この場合、SIGSEGVシグナルのデフォルトの動作のために子が終了したことがわかります。

親プロセスは、メッセージを出力することにより、イベントを人間に報告できます。シェルプログラムはほとんど常にこれを行います。あなたはcrshそれを行うためのコードが含まれていませんが、Cライブラリルーチンがあるため、それは、とにかく起こるsystem、フル機能のシェルを実行している/bin/sh「フードの下」、。このシナリオcrsh祖父母です。親プロセス通知はによってフィールド化され/bin/sh、通常のメッセージを出力します。その後/bin/sh、何もすることがないため、それ自体が終了し、Cライブラリの実装systemその終了通知を受け取ります。の戻り値を調べることにより、コードで終了通知を確認できます。system; しかし、中間プロセスで消費されたため、孫プロセスがセグメンテーション違反で死亡したことはわかりません。


脚注

  1. 一部のオペレーティングシステムは、カーネルの一部としてデバイスドライバーを実装していません。しかし、全ての割り込みハンドラは、まだカーネルの一部でなければならない、そしてハードウェアは何もできないためので、メモリ保護を設定するコードを行いますが、これらの事を行うには、カーネルを。

  2. カーネルよりもさらに特権のある「ハイパーバイザー」または「仮想マシンマネージャー」と呼ばれるプログラムがあるかもしれませんが、この答えの目的のために、それはハードウェアの一部と考えることができます。

  3. カーネルはプログラムですが、プロセスではありません。それは図書館のようなものです。すべてのプロセスは、独自のコードに加えて、カーネルのコードの一部を時々実行します。カーネルコードのみを実行する「カーネルスレッド」が多数存在する場合がありますが、ここでは関係ありません。

  4. 1、あなたはもう対処しなければならない可能性があるだけでOS ができないのUnixの実装と考えることは、当然のことながら、Windowsのです。この状況では信号を使用しません。(実際に、それはありません持っている信号を、Windows上の<signal.h>インターフェースは完全にCライブラリによって偽造されています。)これは「と呼ばれる何か使用構造化例外処理の代わりを」。

  5. SIGBUS代わりに、いくつかのメモリ保護違反が発生します(「バスエラー」)SIGSEGV。2つの間の線は指定不足で、システムによって異なります。のハンドラを定義するプログラムを作成したSIGSEGV場合は、おそらく同じハンドラを定義することをお勧めしますSIGBUS

  6. 「セグメンテーションフォールト」は、元のUnixを実行したコンピューターの1つ、おそらくPDP-11によってメモリ保護違反に対して生成された割り込みの名前でした。「セグメンテーション」はメモリ保護の一種ですが、今日では「セグメンテーション障害」という用語は一般的にあらゆる種類のメモリ保護違反を指します。

  7. 子プロセスが終了したことを親プロセスに通知する他のすべて方法では、親プロセスが呼び出しwaitて終了ステータスを受け取ります。何か他のものが最初に起こるだけです。


@zvol:ad 2)CPUはプロセスについて何でも知っていると言うのは正しいとは思わない。割り込みハンドラを呼び出し、制御を転送すると言う必要があります。
user323094

9
@ user323094最近のマルチコアCPUは、実際にはプロセスについてかなりよく知っています。この状況では、メモリ保護違反をトリガーした実行スレッドのみを一時停止できるように十分です。また、低レベルの詳細に入らないようにしました。ユーザースペースプログラマーの観点から、ステップ2について理解する最も重要なことは、メモリ保護の違反を検出するのはハードウェアであることです。「問題のあるプロセス」を特定する際に、ハードウェア、ファームウェア、およびオペレーティングシステムの間で正確に分業する必要はありません。
zwol

素朴な読者を混乱させる可能性があるもう1つの微妙な点は、「カーネルが問題のプロセスにSIGSEGVシグナルを送信する」です。これは、通常の専門用語を使用しますが、実際にカーネルが告げることを意味自体を(インストールシグナルハンドラ、カーネルによって解決される問題がなければ、ユーザランドのコードが巻き込まれませんIE)プロセス・バー上の信号fooで対処します。そのため、「プロセスでSIGSEGVシグナルを発生させる」ことを好む場合があります。
dmckee

2
SIGBUS(バスエラー)とSIGSEGV(セグメンテーションフォールト)の大きな違いは次のとおりです。SIGSEGVは、CPU アドレスにアクセスしはならないこと認識したときに発生します(したがって、外部メモリバス要求を行いません)。SIGBUSが発生するのは、CPUが外部アドレスバスにリクエストを送信した後、アドレス指定の問題についてCPUが検出した場合のみです。例えば、バス上の何も応答する物理アドレスを求め、または(1の代わりに取得するために2つの物理的な要求を必要とする)誤整列境界上のデータを読み取るために求めて
スチュアートCAIE

2
@StuartCaie 割り込みの動作を説明しています。実際、多くのCPUはアウトラインを区別しています(ただし、一部のCPUはそうではなく、2つの間の線は異なります)。信号 SIGSEGVおよびSIGBUSは、しかし、されていない確実これら二つのCPUレベルの条件にマッピング。POSIXがSIGSEGVではなくSIGBUSを必要とする唯一の条件はmmap、ファイルよりも大きいメモリ領域にファイルを配置し、ファイルの末尾を超えて「全ページ」にアクセスする場合です。(POSIXは、SIGSEGV / SIGBUS / SIGILL / etcがいつ発生するかについて非常にあいまいです。)
zwol

42

シェルは確かにそのメッセージと関係がありcrsh、おそらく間接的にシェルを呼び出しますbash

私は常にフォールトをセグする小さなCプログラムを書きました。

#include <stdio.h>

int
main(int ac, char **av)
{
        int *i = NULL;

        *i = 12;

        return 0;
}

デフォルトのシェルから実行するとzsh、次のようになります。

4 % ./segv
zsh: 13512 segmentation fault  ./segv

から実行するとbash、質問であなたが指摘したことがわかります:

bediger@flq123:csrc % ./segv
Segmentation fault

私は自分のコードにシグナルハンドラを書くつもりだったのですが、それからexecがsystem()使用するライブラリ呼び出しcrshがシェルに/bin/sh基づいてman 3 systemいることに気付きました。確かにそうではない/bin/shので、それはほぼ確実に「セグメンテーション違反」を印刷してcrshいます。

システムコールcrshを使用しexecve()てプログラムを実行するように書き直した場合、「セグメンテーションエラー」文字列は表示されません。これは、によって呼び出されたシェルから取得されsystem()ます。


5
これについては、ディートリッヒ・エップと話し合っていました。私は一緒にハッキング使用crshのバージョンexecvpシェルはまだ(SIGSEGVがシェルに送られることはありませんという意味)がクラッシュしませんが、それがないことが判明して再度テストをしていたではない「セグメンテーション違反」を印刷します。何も印刷されません。これは、シェルがその子プロセスが強制終了したことを検出し、「セグメンテーションフォールト」(またはそのバリアント)を出力する責任があることを示しているようです。
ブレーデンベスト

2
@BradenBest-私は同じことをしました。私のコードはあなたのコードよりもゆるいです。私はまったくメッセージを受け取りませんでしたし、私のクラッピーシェルも物を印刷しません。waitpid()各fork / execで使用しましたが、ステータス0で終了するプロセスとは異なり、セグメンテーションフォールトがあるプロセスに対して異なる値を返します。
ブルースエディガー16年

21

「CPUのMMUが信号を送信する」と「カーネルが問題のあるプログラムにそれを送り、それを終了する」以外に、これに関する情報を見つけることができないようです。

これは少し不明瞭な要約です。Unixシグナルメカニズムは、プロセスを開始するCPU固有のイベントとはまったく異なります。

一般に、不正なアドレスにアクセスした場合(または読み取り専用領域に書き込んだ場合、実行不可能なセクションを実行しようとした場合など)、CPUはCPU固有のイベントを生成します(従来の非VMアーキテクチャでは、各「セグメント」(従来、読み取り専用の実行可能「テキスト」、書き込み可能および可変長の「データ」、および従来メモリの反対側にあるスタック)には固定範囲のアドレスがあったため、セグメンテーション違反と呼ばれます-最新のアーキテクチャでは、ページフォールト[マップされていないメモリの場合]またはアクセス違反[読み取り、書き込み、実行の許可の問題]の可能性が高いため、残りの回答ではこれに焦点を当てます)。

さて、この時点で、カーネルはいくつかのことができます。ページフォールトは、有効ではあるがロードされていないメモリ(スワップアウト、またはマップされたファイルなど)に対しても生成されます。この場合、カーネルはメモリをマップし、原因となった命令からユーザープログラムを再起動します。エラー。それ以外の場合は、シグナルを送信します。シグナルハンドラーをインストールするプロセスが異なり、プログラムに割り込みハンドラーのインストールをシミュレートすることが期待されていた場合は、アーキテクチャにほとんど依存しないため、これは正確に[元のイベント]を問題のプログラムに向けません。

ユーザープログラムにシグナルハンドラーがインストールされている場合、これはスタックフレームを作成し、ユーザープログラムの実行位置をシグナルハンドラーに設定することを意味します。同じことがすべての信号に対して行われますが、セグメンテーション違反の場合、信号ハンドラーが返された場合にエラーの原因となった命令を再開するように、一般的に状況が整えられます。ユーザープログラムがエラーを修正した可能性があります。たとえば、メモリを問題のあるアドレスにマッピングすることで、これが可能かどうかはアーキテクチャに依存します)。シグナルハンドラーは、プログラム内の別の場所にジャンプして(通常はlongjmpを介して、または例外をスローして)、不正なメモリアクセスの原因となった操作を中止することもできます。

ユーザープログラムにシグナルハンドラーがインストールされていない場合、単純に終了します。一部のアーキテクチャでは、信号が無視されると、命令が何度も再起動され、無限ループが発生する場合があります。


+1、受け入れられたものに何かを追加する回答のみ。「セグメンテーション」履歴のわかりやすい説明。楽しい事実:x86は、32ビット保護モード(ページング(仮想メモリ)が有効または無効)でセグメント制限を実際に持っているため、メモリにアクセスする命令は#PF(fault-code)(ページフォールト)または#GP(0)(「メモリオペランドの実効アドレスがCSの外にある場合、 DS、ES、FS、またはGSセグメントの制限。」)。64ビットモードでは、OSは代わりにページングを使用しただけなので、セグメント制限チェックは削除され、ユーザー空間にはフラットメモリモデルが使用されます。
ピーターコーデス

実際、x86上のほとんどのOSはセグメント化されたページネーションを使用していると思います。フラットなページ化されたアドレス空間内の大きなセグメントの集まりです。これは、カーネルメモリを保護して各アドレス空間にマップする方法です。リング(保護レベル)はページではなくセグメントにリンクされます
ロレンツォデマット

また、NTでは(ただし、ほとんどのUnixで同じかどうかを知りたい!)「セグメンテーションフォールト」が頻繁に発生する可能性があります。ユーザー空間の先頭に64kの保護セグメントがあるため、 (適切?)セグメンテーションフォールト
ロレンツォデマテ

1
@LorenzoDemattéはい、すべてまたはほぼすべての最新のUnixは、NULL参照解除をキャッチするために、アドレス空間の先頭に永続的にマッピングされていないアドレスのチャンクを残します。64ビットシステムでは、実際には4 ギガバイトになる可能性があるため、32ビットへのポインターの誤った切り捨てが即座に検出されます。ただし、厳密なx86の意味でのセグメンテーションはほとんど使用されません。ユーザースペース用とカーネル用に1つのフラットセグメントがあり、FSとGSを使用するなどの特別なトリック用に2つあります。
zwol

1
@LorenzoDemattéNTは、シグナルではなく例外を使用します。この場合、STATUS_ACCESS_VIOLATION。
Random832

18

セグメンテーションフォールトは、許可されていないメモリアドレスへのアクセスです(プロセスの一部ではない、読み取り専用データを書き込もうとしている、実行不可能なデータを実行するなど)。これはMMU(メモリ管理ユニット、今日のCPUの一部)によってキャッチされ、割り込みを引き起こします。割り込みはカーネルによって処理され、カーネルは問題のプロセスにSIGSEGFAULTシグナル(signal(2)たとえば、参照)を送信します。このシグナルのデフォルトのハンドラーはコアをダンプし(を参照core(5))、プロセスを終了します。

シェルにはこれにはまったく関係がありません。


3
あなたのCライブラリは、デスクトップ上のglibcのように、文字列を定義していますか?
ドリューベン

7
SIGSEGV 処理/無視できることも注目に値します。そのため、それによって終了されないプログラムを作成することが可能です。:Java仮想マシンは、ここで述べたように、異なる目的のために内部的にSIGSEGVを使用して1つの顕著な例であるstackoverflow.com/questions/3731784/...
カロル貰える

2
同様に、Windowsでは、.NETはほとんどの場合、nullポインターチェックを追加しません-アクセス違反(segfaultと同等)をキャッチするだけです。
イミビス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.