web-dev-qa-db-ja.com

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

私は、末尾呼び出しが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例外:次のいずれかで終了します。

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

逆に、 'Optimize build' ON +(Target = x64またはAnyCPUwith'Prefer 32bit 'OFF(64bit CPU))を使用すると、TCEが発生し、カウンターが永久に回転し続けます(OK、間違いなく回転しますdownその値がオーバーフローするたび)。

しかし、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.

そしてデバッグビルド:

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に触れる、相互運用、および場合によっては非決定論的なロックの問題を提起します。

だから私はこれにコードを簡略化しました:

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

デバッガーなしでリリース/ 32ビット/最適化オンで実行しました。プログラムがクラッシュしたら、デバッガーを接続してカウンターの値を確認します。

そしてそれまだいくつかの実行で同じではありません。 (または私のテストに欠陥があります。)

更新:閉鎖

Fejesjocoが提案したように、私はASLR(アドレス空間配置のランダム化)を調べました。

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

理論は良さそうです。それを実践しましょう!

これをテストするために、タスクに固有のMicrosoftツールを使用しました: EMETまたはEnhanced Mitigation Experience Toolkit 。これにより、システムレベルまたはプロセスレベルでASLRフラグ(およびその他多く)を設定できます。
システム全体のレジストリハッキングの代替手段 私が試していなかったものもあります)

EMET GUI

ツールの有効性を検証するために、 Process Explorer がプロセスの[プロパティ]ページでASLRフラグのステータスを適切に報告していることも発見しました。今日までそれを見たことがない:)

enter image description here

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

ただし、システム全体でASLRを無効にし、(後で1回再起動すると)最終的に、SO例外が常に同じスタック深度で発生することを確認できました。

ボーナス

ASLR関連、古いニュース: How Chrome pwned

74

[〜#〜] aslr [〜#〜] 仕事中かもしれないと思います。 DEPをオフにして、この理論をテストできます。

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

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

更新:OK、今私は正しいことを知っています。ハーフページ理論をフォローアップしたところ、WindowsでのASLR実装を検証するこのドキュメントが見つかりました: http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf

見積もり:

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

そして、これがあなたの質問に対する答えです。 ASLRは、初期スタックの0〜2048バイトをランダムに削除します。

51
fejesjoco