Warum verursacht ein rekursiver Aufruf einen StackOverflow mit unterschiedlichen Stapeltiefen?

Ich habe versucht, praktisch herauszufinden, wie Tail-Aufrufe vom C # -Compiler verarbeitet werden.

(Antworten:Sie sind nicht. Aber die64-Bit-JIT (s) WIRD TCE (Tail Call Elimination) machen.Einschränkungen gelten.)

Also habe ich einen kleinen Test mit einem rekursiven Aufruf geschrieben, der ausgibt, wie oft er vor dem aufgerufen wirdStackOverflowException bricht den Prozess ab.

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();
    }

Pünktlich endet das Programm mit SO Exception in einem der folgenden Fälle:

'Build optimieren' AUS (entweder Debug oder Release)Ziel: x86Ziel: AnyCPU + "Prefer 32 bit" (dies ist neu in VS 2012 und das erste Mal, dass ich es gesehen habe.Mehr hier.)Eine scheinbar harmlose Verzweigung im Code (siehe kommentierte 'else'-Verzweigung).

Umgekehrt passiert bei Verwendung von "Build optimieren" (ON +) (Target = x64 oder AnyCPU mit "Prefer 32bit" (OFF) (auf einer 64bit-CPU)) TCE und der Zähler dreht sich für immer (ok, es dreht sich wahrscheinlich)Nieder jedes Mal, wenn sein Wert überläuft).

Aber ich habe ein Verhalten bemerkt, das ich nicht erklären kann in demStackOverflowException case: es passiert nie (?) umgenau die gleiche Stapeltiefe. Hier sind die Ausgaben einiger 32-Bit-Läufe, Release build:

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.

Und Debug-Build:

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.

Die Stapelgröße ist konstant (Standardeinstellung ist 1 MB). Die Stapelrahmengrößen sind konstant.

Also, was kann für die (manchmal nicht triviale) Variation der Stapeltiefe verantwortlich sein, wenn dieStackOverflowException Treffer?

AKTUALISIEREN

Hans Passant wirft das Thema aufConsole.WriteLine Berühren von P / Invoke, Interop und möglicherweise nicht deterministischer Sperre.

Also habe ich den Code folgendermaßen vereinfacht:

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

Ich habe es in Release / 32bit / Optimization ON ohne Debugger ausgeführt. Wenn das Programm abstürzt, hänge ich den Debugger an und überprüfe den Wert des Zählers.

Und esimmer noch ist nicht auf mehreren Läufen gleich. (Oder mein Test ist fehlerhaft.)

UPDATE: Schließung

Wie von fejesjoco vorgeschlagen, habe ich mich mit ASLR (Address Space Layout Randomization) befasst.

Es ist eine Sicherheitstechnik, die es Pufferüberlaufangriffen erschwert, den genauen Ort (z. B.) bestimmter Systemaufrufe zu finden, indem verschiedene Dinge im Prozessadressraum, einschließlich der Stapelposition und anscheinend seiner Größe, zufällig ausgewählt werden.

Die Theorie hört sich gut an. Lassen Sie es uns in die Praxis umsetzen!

Um dies zu testen, habe ich ein für die Aufgabe spezifisches Microsoft-Tool verwendet:EMET oder das Enhanced Mitigation Experience Toolkit. Es ermöglicht das Setzen des ASLR-Flags (und vieles mehr) auf System- oder Prozessebene.
(Da ist auch einSystemweite Alternative zum Registry-Hacking das habe ich nicht versucht)

Um die Wirksamkeit des Tools zu überprüfen, habe ich auch festgestellt, dassProcess Explorer meldet ordnungsgemäß den Status des ASLR-Flags auf der Seite "Eigenschaften" des Prozesses. Habe das bis heute nicht gesehen :)

Theoretisch kann EMET das ASLR-Flag für einen einzelnen Prozess (neu) setzen. In der Praxis schien sich daran nichts zu ändern (siehe Bild oben).

Allerdings habe ich ASLR für das gesamte System deaktiviert und (ein Neustart später) konnte ich endlich überprüfen, dass die SO-Ausnahme nun tatsächlich immer mit der gleichen Stapeltiefe auftritt.

BONUS

ASLR-bezogen, in älteren Nachrichten:Wie Chrome wurde pwned

Antworten auf die Frage(2)

Ihre Antwort auf die Frage