web-dev-qa-db-ja.com

StackOverflowExceptionはどのように検出されますか?

TL; TR
質問をしたとき、StackOverflowExceptionはアプリケーションが無限に実行されるのを防ぐメカニズムであると想定しました。本当じゃない。
A StackOverflowExceptionは検出されていません。
スタックに、より多くのメモリを割り当てる容量がない場合にスローされます。

[元の質問:]

これは一般的な質問であり、プログラミング言語ごとに回答が異なる場合があります。
C#以外の言語がスタックオーバーフローを処理する方法がわかりません。

今日は例外を経験していましたが、StackOverflowExceptionの検出方法について考え続けました。私はそれを言うことは不可能だと信じていますスタックが1000コールの深さである場合、例外をスローします。場合によっては、正しいロジックがそれほど深くなるからです。

私のプログラムの無限ループの検出の背後にあるロジックは何ですか?

StackOverflowExceptionクラス:
https://msdn.Microsoft.com/de-de/library/system.stackoverflowexception%28v=vs.110%29.aspx

StackOverflowExceptionクラスのドキュメントに記載されている相互参照:
https://msdn.Microsoft.com/de-de/library/system.reflection.emit.opcodes.localloc(v = vs.110).aspx

stack-overflowこの質問にタグを付けます。説明では、コールスタックが大量のメモリを消費するとスローされると説明されています。それは、呼び出しスタックが私のプログラムの現在の実行位置へのある種のパスであり、それ以上のパス情報を保存できない場合、例外がスローされることを意味しますか?

67
Noel Widmer

スタックオーバーフロー

簡単にします。しかし、これは実際には非常に複雑です...ここでかなり一般化することに注意してください。

ご存じかもしれませんが、ほとんどの言語はスタックを使用して呼び出し情報を保存します。参照: https://msdn.Microsoft.com/en-us/library/zkwh89ks.aspx cdeclの仕組みについて。メソッドを呼び出すと、スタックにプッシュされます。戻ったら、スタックからものをポップします。

再帰は通常「インライン」ではないことに注意してください。 (注:ここでは明示的に「再帰」と言いますが、「末尾再帰」ではありません。後者は「goto」のように機能し、スタックを拡張しません)。

スタックオーバーフローを検出する最も簡単な方法は、現在のスタックの深さ(使用バイト数など)を確認することです-境界に達するとエラーが発生します。この「境界チェック」について明確にするために:これらのチェックが行われる方法は、通常ガードページを使用することです。これは、境界チェックが通常if-then-elseチェックとして実装されていないことを意味します(一部の実装は存在しますが...)。

ほとんどの言語では、各スレッドには独自のスタックがあります。

無限ループの検出

さて、ここでしばらく聞いていない質問があります。 :-)

基本的にすべての無限ループを検出するには、 Halting Problem を解決する必要があります。ちなみに 決定不能な問題 です。これはコンパイラーによって絶対に行われません。

これは、分析ができないという意味ではありません。実際、かなりの分析を行うことができます。ただし、場合によっては(Webサーバーのメインループなど)無限に実行したいこともあります。

他の言語

また興味深い...関数型言語は再帰を使用するため、基本的にはスタックによってバインドされます。 (そうは言っても、関数型言語は末尾再帰を使用する傾向があり、これは多かれ少なかれ「goto」のように機能し、スタックを成長させません。)

そして、論理言語があります..まあ、それで永遠にループする方法がわかりません-あなたはおそらくまったく評価されないものになるでしょう(解決策は見つかりません)。 (ただし、これはおそらく言語に依存します...)

降伏、非同期、継続

おもしろい概念は、あなたが考えるかもしれないことです continuations と呼ばれます。 Microsoftからyieldが最初に実装されたとき、実際の継続が実装と見なされたと聞いたことがあります。継続により、基本的にスタックを「保存」し、別の場所に継続し、後でスタックを「復元」することができます...

残念ながら、Microsoftはこのアイデアを採用しませんでした(理由は想像できますが)が、ヘルパークラスを使用して実装しました。 C#でのイールドと非同期は、一時クラスを追加し、クラス内のすべてのローカル変数をインターンすることで機能します。 「yield」または「async」を実行するメソッドを呼び出す場合、実際には、ヒープにプッシュされるヘルパークラスを(呼び出したメソッド内から、スタックにプッシュして)作成します。ヒープにプッシュされるクラスには機能があります(たとえば、yieldの場合、これは列挙型実装です)。これを行う方法は、MoveNextが呼び出されたときにプログラムが継続する場所(たとえば、ある状態ID)を格納する状態変数を使用することです。このIDを使用するブランチ(スイッチ)が残りを処理します。このメカニズムは、スタック自体の動作に「特別な」ことは何もしないことに注意してください。クラスとメソッドを使用して、自分で同じものを実装できます(入力が増えるだけです:-))。

手動スタックでスタックオーバーフローを解決する

私はいつも良い洪水が好きです。これを間違えた場合、画像は多くの再帰呼び出しの地獄を与えます...このように言います:

public void FloodFill(int x, int y, int color)
{
    // Wait for the crash to happen...
    if (Valid(x,y))
    {
        SetPixel(x, y, color);
        FloodFill(x - 1, y, color);
        FloodFill(x + 1, y, color);
        FloodFill(x, y - 1, color);
        FloodFill(x, y + 1, color);
    }
}

ただし、このコードには何の問題もありません。すべての作業を行いますが、スタックが邪魔になります。実装が基本的に同じであっても、手動スタックを使用するとこれが解決されます。

public void FloodFill(int x, int y, int color)
{
    Stack<Tuple<int, int>> stack = new Stack<Tuple<int, int>>();
    stack.Push(new Tuple<int, int>(x, y));
    while (stack.Count > 0)
    {
        var current = stack.Pop();

        int x2 = current.Item1;
        int y2 = current.Item2;

        // "Recurse"
        if (Valid(x2, y2))
        {
            SetPixel(x2, y2, color);
            stack.Push(new Tuple<int, int>(x2-1, y2));
            stack.Push(new Tuple<int, int>(x2+1, y2));
            stack.Push(new Tuple<int, int>(x2, y2-1));
            stack.Push(new Tuple<int, int>(x2, y2+1));
        }
    }
}
42
atlaste

ここにはすでに多くの答えがあり、その多くは要点を理解し、その多くは微妙なまたは大きなエラーを持っています。ゼロからすべてを説明しようとするのではなく、いくつかの重要な点を説明します。

C#以外の言語がスタックオーバーフローをどのように処理するかわかりません。

あなたの質問は、「スタックオーバーフローはどのように検出されますか?」です。 C#または他の言語でどのように検出されるかについての質問はありますか?別の言語について質問がある場合は、新しい質問を作成することをお勧めします。

(たとえば)スタックの深さが1000コールである場合に例外をスローすることはできないと思います。場合によっては、正しいロジックがそれほど深くなるからです。

そのようなスタックオーバーフロー検出を実装することは絶対に可能です。実際には、これはそれが行われる方法ではありませんが、システムをそのように設計できなかった理由はありません。

プログラムの無限ループの検出の背後にあるロジックは何ですか?

無限ループではなく、無制限の再帰を意味します。

以下で説明します。

この質問にstack-overflowタグを追加しましたが、説明では、呼び出しスタックがメモリを大量に消費するとスローされると説明しています。それは、呼び出しスタックが私のプログラムの現在の実行位置への何らかのパスであり、それ以上のパス情報を保存できない場合、例外がスローされることを意味しますか?

簡単な答え:はい。

より長い答え:呼び出しスタックは2つの目的に使用されます。

まず、アクティベーション情報を表します。つまり、ローカル変数の値と、その有効期間がメソッドの現在のアクティブ化(「呼び出し」)以下である一時値。

第二に、継続情報を表します。つまり、このメソッドを使い終わったら、次に何をする必要がありますか?スタックはnotが「どこから来たのか?」を表すことに注意してください。スタックは次はどこに行くかを表し、メソッドが戻ると通常になり、元の場所に戻ります。

スタックには、非ローカル継続の情報、つまり例外処理も格納されます。メソッドがスローされると、コールスタックには、関連するcatchブロックを含むコードがある場合、ランタイムがそれを判断するのに役立つデータが含まれます。そのcatchブロックは、メソッドのcontinuation-"次に何をするか"-になります。

さて、先に進む前に、コールスタックはtwoの目的で使用されているデータ構造であり、単一の責任原則に違反していることに注意してください。 要件はありません。2つの目的に使用される1つのスタックがあります。実際には、アクティブ化フレーム用とリターンアドレス用の2つのスタック(具体化である)このようなアーキテクチャは、Cのような言語で発生する可能性のある「スタックスマッシング」攻撃に対して脆弱ではありません。

メソッドを呼び出すと、スタックにメモリが割り当てられ、リターンアドレス(次に何をするか)とアクティベーションフレーム(新しいメソッドのローカル)が格納されます。 Windowsのスタックはデフォルトで固定サイズであるため、十分なスペースがない場合、悪いことが起こります。

より詳細には、Windowsはどのようにスタック検出を行いますか?

1990年代に32ビットWindowsバージョンのVBScriptおよびJScriptのスタック外検出ロジックを作成しました。 CLRは私が使用したのと同様の手法を使用しますが、CLR固有の詳細を知りたい場合は、CLRの専門家に相談する必要があります。

32ビットWindowsだけを考えてみましょう。 64ビットWindowsも同様に機能します。

もちろん、Windowsは仮想メモリを使用します。仮想メモリの仕組みがわからない場合は、読み進める前に学習するのがよいでしょう。各プロセスには32ビットのフラットアドレス空間が与えられ、半分はオペレーティングシステム用に、残りはユーザーコード用に予約されています。デフォルトでは、各スレッドには、1 MBのアドレス空間の予約済み連続ブロックが与えられます。 (注:これは、スレッドが重い理由の1つです。そもそも20億バイトしかない場合、100万バイトの連続したメモリが大量にあります。)

ここでは、連続したアドレススペースが単に予約されているか、実際にコミットされているかに関して微妙な点がいくつかありますが、それらについて詳しく説明します。 CLRの詳細を説明するのではなく、従来のWindowsプログラムでどのように機能するかを引き続き説明します。

さて、100万バイトのメモリを、それぞれ4 KBの250ページに分割するとしましょう。しかし、プログラムが最初に実行を開始するときに必要なのは、おそらく数キロバイトのスタックだけです。だから、ここでそれがどのように機能するかです。現在のスタックページは、完全にコミットされたページです。それはただの普通の記憶です。保護ページとしてマークされているページbeyond。そして、100万バイトスタックのlastページは、非常に特別なガードページとしてマークされています。

良いスタックページを超えて1バイトのスタックメモリを書き込もうとしているとします。そのページは保護されているため、ページフォールトが発生します。オペレーティングシステムは、そのスタックページを正常にすることで障害を処理し、nextページが新しいガードページになります。

ただし最後のガードページがヒットした場合-非常に特殊なもの-Windowsはスタック外例外をトリガーし、Windowsはガードページをリセットして、 「このページが再度ヒットした場合、プロセスを終了します」。その場合、Windowsはプロセスを終了します即時。例外なし。クリーンアップコードはありません。ダイアログボックスはありません。 Windowsアプリが突然完全に消えてしまうのを見たことがあれば、おそらくスタックの最後のガードページに誰かがsecondの時間ヒットしたのでしょう。

さて、メカニズムを理解できたので、ここで詳細を詳しく説明しますが、スタック外の例外を発生させるコードの書き方がわかるでしょう。 VBScriptとJScriptで行った丁寧な方法は、スタックで仮想メモリクエリを実行し、最終的なガードページの場所を尋ねることです。その後、定期的に現在のスタックポインターを確認し、数ページ以内に到達している場合は、オペレーティングシステムに任せるのではなく、VBScriptエラーを作成するか、その場でJavaScript例外をスローします。

プローブを自分で行いたくない場合は、最後のガードページがヒットしたときにオペレーティングシステムが提供するfirst chance例外を処理できます。これをC#のスタックオーバーフロー例外に変換します。理解し、非常に注意にして、ガードページに2度目にヒットしないようにします。

33
Eric Lippert

スタックは、スレッドの作成時に割り当てられる固定サイズのメモリブロックです。 「スタックポインター」もあります。これは、現在使用されているスタックの量を追跡する方法です。新しいスタックフレームの作成の一部として(メソッド、プロパティ、コンストラクターなどを呼び出すとき)、新しいフレームが必要とする量だけスタックポインターを上に移動します。その時点で、スタックポインターがスタックの末尾を超えて移動したかどうかを確認し、移動している場合は、SOEをスローします。

プログラムは無限再帰を検出するために何もしません。無限再帰(ランタイムが呼び出しごとに新しいスタックフレームを作成することを強制されるとき)は、この有限スペースを埋めるために実行されるメソッド呼び出しが非常に多くなるだけです。スタックが持っているよりも多くのスペースを消費する、ネストされたメソッド呼び出しの有限数でその有限スペースを簡単に埋めることができます。 (しかし、これはやや難しい傾向があります;通常、再帰的であり、無限ではないメソッドによって引き起こされますが、スタックが処理できないほど十分な深さです。)

14
Servy

警告:これにはlotがあり、CLR自体の動作方法など、ボンネットの下のメカニズムと関係があります。これは、アセンブリレベルのプログラミングの学習を開始する場合にのみ意味があります。

内部では、メソッド呼び出しは、制御を別のメソッドのサイトに渡すことによって実行されます。引数と戻り値を渡すために、これらはスタックにロードされます。呼び出しメソッドへのreturn制御の方法を知るために、CLRはcall stack、メソッドが呼び出されたときにプッシュされ、メソッドが戻ったときにポップされます。このスタックは、制御を返す先のメソッドwhereに指示します。

コンピューターのメモリは有限であるため、呼び出しスタックが大きくなりすぎる場合があります。したがって、StackOverflowExceptionnot無限に実行中または無限に再帰的なプログラムの検出であり、メソッドが戻る必要がある場所、必要な引数、戻り値、変数、または(より一般的には)それらの組み合わせを追跡するために必要なスタックのサイズをコンピューターが処理できないことを検出します。この例外が無限再帰中に発生するという事実は、ロジックが必然的にスタックを圧倒するためです。

あなたの質問に答えるために、プログラムが意図的にスタックをオーバーロードするロジックを持っている場合、はいStackOverflowExceptionが表示されます。ただし、これは通常、数千コールミリオンまでの深さであり、無限再帰ループを作成しない限り、実際の問題になることはほとんどありません。

補遺:recursiveループに言及する理由は、スタックをオーバーホールした場合にのみ例外が発生するためです-これは通常、最終的に呼び出すメソッドを呼び出していることを意味します同じメソッドに戻り、呼び出しスタックを増やします。論理的に無限であるものの、not再帰的なものがある場合、通常、StackOverflowExceptionは表示されません。

6
David

スタックオーバーフローの問題は、無限の計算に起因する可能性があるということではありません。問題は、今日のオペレーティングシステムと言語における有限のリソースであるスタックメモリの枯渇です。

この条件は、プログラムがスタックに割り当てられている範囲を超えるメモリの一部にアクセスしようとしたときに検出されます。これにより例外が発生します。

4
usr

サブ質問のほとんどは十分に回答されています。スタックオーバーフロー状態の検出に関する部分を明確にしたいと思います。できれば、Eric Lippertの答え(もちろん正しいですが、不必要に複雑になっています)よりも理解しやすい方法でください。代わりに、私は答えを畳み込みます別の方法で、1つではなく2つの異なるアプローチに言及します。

スタックオーバーフローを検出するには、コードを使用する方法とハードウェアを使用する方法の2つがあります。

コードを使用したスタックオーバーフロー検出 PCが16ビットrealモードとハードウェアで実行されていた時代に使用されていました弱虫でした。これはもう使用されていませんが、言及する価値があります。このシナリオでは、作成する各関数の先頭に、スタックチェックコードの特別な非表示部分を出力するようコンパイラーに要求するコンパイラースイッチを指定します。このコードは、単にスタックポインタレジスタの値を読み取り、スタックの終わりに近すぎるかどうかを確認します。もしそうなら、それは私たちのプログラムを停止します。 x86アーキテクチャのスタックは下方に増加するため、アドレス範囲0x80000から0x90000がプログラムのスタックとして指定されている場合、スタックポインターは最初は0x90000を指し、ネストされた関数を呼び出し続けると0x80000に向かって下がります。そのため、スタックポインターが0x80000に近すぎる(たとえば、0x80010以下)ことをスタックチェックコードが検出すると、停止します。

これにはすべて、a)行うすべての関数呼び出しにオーバーヘッドが追加され、b)その特別なコンパイラスイッチでコンパイルされなかったために実行されない外部コードの呼び出し中にスタックオーバーフローを検出できないという欠点がありますスタックオーバーフローチェック。当時のStackOverflow例外は前代未聞の贅沢でした。プログラムは非常に簡潔で終了するでしょう(ほとんど(rude)エラーメッセージ、またはシステムのクラッシュがあり、再起動が必要です。

ハードウェアの助けを借りたスタックオーバーフロー検出基本的にジョブをCPUに委任します。最新のCPUには、メモリをページ(通常は各4KB)に分割し、特定のページにアクセスしたときに割り込み(「トラップ」と呼ばれる一部のアーキテクチャ)を自動的に発行する機能など、ページごとにさまざまなトリックを実行する精巧なシステムがあります。そのため、オペレーティングシステムは、割り当てられた最小値未満のスタックメモリアドレスにアクセスしようとした場合に割り込みが発行されるようにCPUを構成します。その割り込みが発生すると、言語のランタイム(C#の場合は.Netランタイム)によって受信され、StackOverflow例外に変換されます。

これには、余分なオーバーヘッドがまったくないという利点があります。 CPUが常に実行しているページ管理に関連するオーバーヘッドがありますが、仮想メモリが動作するために必要であり、1つのプロセスのメモリアドレス空間を他のプロセスから保護するなどのさまざまなことにより、とにかく支払われますプロセスなど.

3
Mike Nakis