1. ホーム
  2. c#

再帰呼び出しが異なるスタック深度でStackOverflowを引き起こすのはなぜですか?

2023-12-27 14:59:47

質問

私は、C#コンパイラによってテールコールがどのように処理されるかをハンズオンで把握しようとしていました。

(回答 彼らはそうではありません。 しかし、その 64 ビット JIT (複数可) は TCE (テールコールエリミネーション) を実行します。 制限事項の適用 .)

そこで、再帰的な呼び出しを使って小さなテストを書きました。 StackOverflowException がプロセスを停止させるまでに何回呼び出されたかを表示します。

class Program
{
    static void Main(string[] args)
    {
        Rec();
    }

    static int sz = 0;
    static Random r = new Random();
    static void Rec()
    {
        sz++;

        //uncomment for faster, more imprecise runs
        //if (sz % 100 == 0)
        {
            //some code to keep this method from being inlined
            var zz = r.Next();  
            Console.Write("{0} Random: {1}\r", sz, zz);
        }

        //uncommenting this stops TCE from happening
        //else
        //{
        //    Console.Write("{0}\r", sz);
        //}

        Rec();
    }

ちょうどいいタイミングで、SO Exception on any ofでプログラムが終了します。

  • 最適化ビルド」オフ (デバッグまたはリリースのいずれか)
  • ターゲット: x86
  • ターゲット AnyCPU + "Prefer 32 bit"(これはVS 2012で新しくなったもので、初めて見ました。 詳細はこちら .)
  • コード内の一見無害なブランチ (コメントされた 'else' ブランチを参照)。

逆に、'Optimize build' ON + (Target = x64 or AnyCPU with 'Prefer 32bit' OFF (64bit CPU)) を使用すると、TCE が発生し、カウンターは永遠に回転し続けます (OK, it arguably spins) 。 ダウン その値がオーバーフローするたびに回転します)。

しかし、私は説明できない動作に気づきました。 StackOverflowException の場合:それは決して(? まさに で発生することはありません。以下は、いくつかの 32 ビット実行の出力です (リリース ビルド)。

51600 Random: 1778264579
Process is terminated due to StackOverflowException.

51599 Random: 1515673450
Process is terminated due to StackOverflowException.

51602 Random: 1567871768
Process is terminated due to StackOverflowException.

51535 Random: 2760045665
Process is terminated due to StackOverflowException.

そしてDebugビルド。

28641 Random: 4435795885
Process is terminated due to StackOverflowException.

28641 Random: 4873901326  //never say never
Process is terminated due to StackOverflowException.

28623 Random: 7255802746
Process is terminated due to StackOverflowException.

28669 Random: 1613806023
Process is terminated due to StackOverflowException.

スタックサイズは一定で ( のデフォルトは 1 MB です。 ). スタックフレームのサイズは一定です。

では、スタックの深さが(時には自明でないほど)変化するのは、何が原因なのでしょうか。 StackOverflowException がヒットしたときのスタックの深さの変化 (時に非自明) を説明できるでしょうか?

最新情報

Hans Passant は、次のような問題を提起しています。 Console.WriteLine は、P/Invoke、interop、そしておそらく非決定論的ロックに触れています。

そこで、コードをこんな風に簡略化しました。

class Program
{
    static void Main(string[] args)
    {
        Rec();
    }
    static int sz = 0;
    static void Rec()
    {
        sz++;
        Rec();
    }
}

デバッガを使わずにRelease/32bit/Optimization ONで動かしてみました。プログラムがクラッシュした時にデバッガを付けてカウンタの値を確認しています。

すると、それは 今も は何度か実行しても同じではありません。(あるいは私のテストに欠陥があるか)

更新: クロージャ

fejesjocoに提案されたように、私はASLR (Address space layout randomization) について調べました。

これは、スタックの位置やそのサイズなど、プロセスのアドレス空間におけるさまざまなものをランダムにすることにより、バッファ オーバーフロー攻撃による (たとえば) 特定のシステム コールの正確な位置の発見を困難にするセキュリティ技術です。



理論は良さそうですね。実践してみましょう!

これをテストするために、私はこのタスクに特化したMicrosoftのツールを使用しました。 EMET (The Enhanced Mitigation Experience Toolkit、強化された緩和経験ツールキット) . これは、システム レベルまたはプロセス レベルで ASLR フラグ (およびその他多数) を設定することができます。

(また システム全体、レジストリをハックする代替手段 という選択肢もありますが、私は試していません)

また、このツールの効果を検証するために、以下のことを発見しました。 プロセス エクスプローラー は、プロセスの [プロパティ] ページで ASLR フラグの状態をきちんと報告しています。今日まで一度も見たことがありませんでした :)

理論的には、EMET は単一のプロセスに対して ASLR フラグを (再) 設定することができます。実際には、何も変わらないようでした (上の画像参照)。

しかし、私はシステム全体の ASLR を無効にし、(1 回のリブート後に)実際に、SO の例外が常に同じスタック深度で発生することを最終的に確認することができました。

ボーナス

ASLR関連、古いニュースでは Chrome はどのように破壊されたのか

解決方法は?

私はそれがかもしれないと思う ASLR が働いているのではないかと思います。この理論を検証するために DEP をオフにすることができます。

メモリ情報を確認するためのC#ユーティリティクラスはこちらをご覧ください。 https://stackoverflow.com/a/8716410/552139

ちなみにこのツールで、スタックサイズの最大値と最小値の差は約2KiB、つまり半ページ分であることがわかりました。変な話ですね。

更新しました。OK、今、私は自分が正しいことを知りました。半ページの理論を追ったところ、Windows での ASLR 実装を検証したこのドキュメントを見つけました。 http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf

引用元

スタックが配置されると、最初のスタックポインタはさらにランダムな減少量によって ランダムな減少量によってランダム化されます。最初のオフセットは 半ページ (2,048 バイト) までの範囲で選択されます。

そして、これが質問の答えです。ASLR は、初期スタックの 0 バイトから 2048 バイトの間をランダムに取り去ります。