73 votes

Pourquoi un appel récursif provoque-t-il StackOverflow à différentes profondeurs de pile?

J'ai été à essayer de comprendre de manière pratique comment la queue les appels sont gérés par le compilateur C#.

(Réponse: Ils ne sont pas. Mais le 64 bits JIT(s) FERA TCE (queue d'appel de l'élimination). Des Restrictions s'appliquent.)

J'ai donc écrit un petit essai à l'aide d'un appel récursif qui imprime combien de fois elle est appelée avant la StackOverflowException tue le processus.

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

Droit sur la sélection, le programme se termine avec TELLEMENT d'Exception sur l'un de:

  • 'Optimiser le build OFF (Debug ou Release)
  • Cible: x86
  • Cible: AnyCPU + "Préfèrent 32 bits" (ce qui est nouveau dans VS 2012 et la première fois que je l'ai vu. De plus ici.)
  • Certains apparemment anodins de la branche dans le code (voir: "d'autre" branche).

À l'inverse, l'utilisation de 'Optimiser construire" SUR + (Cible = x64 ou AnyCPU avec "Préfèrent 32bit' OFF (sur un CPU 64 bits)), le TCE se passe et le compteur continue de tourner jusqu'à jamais (ok, c'sans doute tours vers le bas chaque fois que sa valeur dépasse).

Mais j'ai remarqué un comportement que je ne peux pas l'expliquer dans l' StackOverflowException cas: il n'a jamais (?) arrive exactement la même pile de profondeur. Voici les résultats de quelques 32 bits s'exécute, Version validée:

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.

Et 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.

La taille de la pile est constante (par défaut à 1 MO). La pile d'images grandeurs sont constantes.

Alors, ce qui peut rendre compte de la (parfois non-trivial) variation de la profondeur de la pile lorsque l' StackOverflowException hits?

Mise à JOUR

Hans Passant soulève la question de l' Console.WriteLine de toucher de P/Invoke, l'interopérabilité et peut-être non-déterministe de verrouillage.

Donc, j'ai simplifié le code pour ceci:

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

J'ai couru dans la Presse/32 bits/Optimisation sans un débogueur. Lorsque le programme se bloque, je attacher le débogueur et vérifier la valeur du compteur.

Et encore n'est-ce pas la même chose sur plusieurs pistes. (Ou mon test est imparfait.)

Mise à JOUR: Fermeture

Comme suggéré par fejesjoco, j'ai regardé dans l'ASLR (Address space layout randomization).

C'est un technique qui rend difficile pour les attaques par débordement de tampon pour trouver l'emplacement précis de (par exemple), des appels système, par randomisation des choses différentes dans l'espace d'adressage du processus, y compris la pile de position et, apparemment, de sa taille.

La théorie, c'est bon. Nous allons le mettre en pratique!

Pour tester cela, j'ai utilisé un outil de Microsoft spécifique pour la tâche: EMET ou de La Enhanced Mitigation Experience Toolkit. Il permet le réglage de l'ASLR drapeau (et beaucoup plus) sur un système ou d'un processus.
(Il est également à l'échelle du système, le registre de piratage alternative que je n'ai pas essayer)

EMET GUI

Afin de vérifier l'efficacité de l'outil, j'ai aussi découvert que Process Explorer dûment rapports sur l'état de l'ASLR drapeau dans les "Propriétés" à la page du processus. N'a jamais vu ça jusqu'à aujourd'hui :)

enter image description here

Théoriquement, IL peut (re)mettre l'ASLR drapeau pour un seul et même processus. Dans la pratique, il ne semble pas de changer quoi que ce soit (voir l'image ci-dessus).

Cependant, j'ai désactivé l'ASLR pour l'ensemble du système et (un redémarrage plus tard), j'ai enfin pu vérifier qu'en effet, exception maintenant arrive toujours à la même pile de profondeur.

BONUS

ASLR liés, dans les anciennes news: Comment Chrome ai pwned

51voto

fejesjoco Points 7142

Je pense que c'est peut être l'ASLR au travail. Vous pouvez désactiver la prévention de tester cette théorie.

Voir ici pour un C# classe utilitaire pour vérifier les informations concernant la mémoire: http://stackoverflow.com/a/8716410/552139

Par ailleurs, avec cet outil, j'ai trouvé que la différence entre le maximum et le minimum de la taille de la pile est d'environ 2 Kio, ce qui est une demi-page. C'est bizarre.

Mise à jour: OK, maintenant je sais que j'ai raison. J'ai suivi sur la demi-page de la théorie, et trouvé cette doc qui examine l'ASLR mise en œuvre dans Windows: http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf

Citation:

Une fois que la pile a été placé, le premier pointeur de pile est plus randomisés par hasard une réduction des changements de quantité. Le décalage initial est sélectionné pour être jusqu'à une demi-page (2048 octets)

Et c'est la réponse à votre question. ASLR enlève entre 0 et 2048 octets de votre stack initial au hasard.

-3voto

Ahmed KRAIEM Points 6414

Changer r.Next() à r.Next(10) . StackOverflowException s devrait se produire à la même profondeur.

Les chaînes générées doivent consommer la même mémoire car elles ont la même taille. r.Next(10).ToString().Length == 1 toujours . r.Next().ToString().Length est variable.

La même chose s'applique si vous utilisez r.Next(100, 1000)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X