Dlaczego wywołanie rekurencyjne powoduje StackOverflow na różnych głębokościach stosu?

Próbowałem dowiedzieć się, w jaki sposób wywołania tail są obsługiwane przez kompilator C #.

(Odpowiedź:Oni nie są. Ale64-bitowe JIT BĘDZIE robić TCE (eliminacja wywołania ogona).Obowiązują ograniczenia.)

Napisałem więc mały test za pomocą wywołania rekurencyjnego, które wyświetla, ile razy zostanie wywołane przedStackOverflowException zabija proces.

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

Na samym początku program kończy się wyjątkiem SO na którymkolwiek z:

„Optymalizuj kompilację” WYŁĄCZ (debugowanie lub zwalnianie)Cel: x86Cel: AnyCPU + „Preferuj 32-bitowy” (jest to nowość w VS 2012 i po raz pierwszy go widziałem).Więcej tutaj.)Jakaś pozornie nieszkodliwa gałąź w kodzie (patrz skomentowana gałąź „inny”).

I odwrotnie, używając „Optimize build” ON + (Target = x64 lub AnyCPU z „Prefer 32bit” OFF (w 64-bitowym CPU)), dzieje się TCE i licznik cały czas się kręci (ok, to prawdopodobnie obraca sięna dół za każdym razem jego wartość przelewa się).

Ale zauważyłem zachowanie, którego nie potrafię wyjaśnić wStackOverflowException sprawa: nigdy (?) się nie dziejedokładnie ta sama głębokość stosu. Oto wyniki kilku uruchomień 32-bitowych, wersja Release:

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.

I kompilacja debugowania:

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.

Rozmiar stosu jest stały (domyślnie 1 MB). Rozmiary ramek stosu są stałe.

Zatem, co może wyjaśnić (czasami nietrywialną) zmienność głębokości stosu, gdyStackOverflowException trafienia?

AKTUALIZACJA

Hans Passant porusza kwestięConsole.WriteLine dotykając P / Invoke, interop i ewentualnie niedeterministyczne blokowanie.

Uprościłem więc kod do tego:

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

Uruchomiłem go w wersji Release / 32bit / Optimization ON bez debugera. Kiedy program się zawiesza, dołączam debuger i sprawdzam wartość licznika.

I tonadal nie jest taki sam w kilku seriach. (Albo mój test jest wadliwy.)

AKTUALIZACJA: Zamknięcie

Jak sugeruje fejesjoco, przejrzałem ASLR (randomizacja układu przestrzeni adresowej).

Jest to technika bezpieczeństwa, która utrudnia atakom z przepełnieniem bufora znalezienie dokładnej lokalizacji (np.) Specyficznych wywołań systemowych, przez losowanie różnych rzeczy w przestrzeni adresowej procesu, w tym pozycji stosu i, najwyraźniej, jego rozmiaru.

Teoria brzmi dobrze. Zastosujmy to w praktyce!

Aby to przetestować, użyłem specyficznego dla tego zadania narzędzia Microsoft:EMET lub zestaw narzędzi Enhanced Mitigation Experience. Pozwala ustawić flagę ASLR (i wiele więcej) na poziomie systemu lub procesu.
(Jest też aogólnosystemowa, hakerska alternatywa dla rejestru że nie próbowałem)

Aby sprawdzić skuteczność narzędzia, odkryłem to równieżProces Explorer należycie zgłasza status flagi ASLR na stronie „Właściwości” procesu. Nigdy tego nie widziałem do dziś :)

Teoretycznie EMET może (ponownie) ustawić flagę ASLR dla pojedynczego procesu. W praktyce nie zmieniło to niczego (patrz powyższy obrazek).

Jednak wyłączyłem ASLR dla całego systemu i (jeden restart później) mogłem wreszcie zweryfikować, że rzeczywiście wyjątek SO zawsze odbywa się na tej samej głębokości stosu.

PREMIA

Związane z ASLR, w starszych wiadomościach:Jak Chrome został pwned

questionAnswers(2)

yourAnswerToTheQuestion