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

J’essayais de comprendre comment les appels de queue sont gérés par le compilateur C #.

Répondre

Donc j’ai écrit un petit test en utilisant un appel récursif qui imprime combien de fois il est appelé avant que StackOverflowException tue le processus.

 class Program { static void Main(ssortingng[] 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(); } 

Au bon moment, le programme se termine par une exception SO sur l’un des éléments suivants:

  • ‘Optimiser la compilation’ OFF (débogage ou publication)
  • Cible: x86
  • Cible: AnyCPU + “Prefer 32 bit” (c’est nouveau dans VS 2012 et la première fois que je l’ai vu. Plus ici .)
  • Une twig apparemment inoffensive dans le code (voir la twig commentée ‘else’).

Inversement, en utilisant ‘Optimiser la compilation’ ON + (Target = x64 ou AnyCPU avec OFF “32bit” OFF (sur un processeur 64 bits)), TCE se produit et le compteur continue de tourner indéfiniment (ok, il tourne indéfiniment chaque fois que sa valeur déborde ).

Mais j’ai remarqué un comportement que je ne peux pas expliquer dans le cas StackOverflowException : il ne se produit jamais (?) Exactement à la même profondeur de stack. Voici les sorties de quelques exécutions 32 bits, version 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. 

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

La taille de la stack est constante ( 1 Mo par défaut ). La taille des frameworks de stack est constante.

Alors, qu’est-ce qui peut expliquer la variation (parfois non sortingviale) de la profondeur de la stack lorsque l’ StackOverflowException ?

METTRE À JOUR

Hans Passant soulève la question de Console.WriteLine touchant P / Invoke, interop et éventuellement le locking non déterministe.

J’ai donc simplifié le code à ceci:

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

Je l’ai lancé dans Release / 32bit / Optimization ON sans débogueur. Lorsque le programme se bloque, je connecte le débogueur et vérifie la valeur du compteur.

Et ce n’est toujours pas la même chose sur plusieurs courses. (Ou mon test est défectueux.)

MISE À JOUR: Fermeture

Comme suggéré par fejesjoco, j’ai examiné ASLR (randomisation de la disposition de l’espace d’adresse).

Il s’agit d’une technique de sécurité qui empêche les attaques par débordement de tampon de trouver l’emplacement exact d’appels système spécifiques (par exemple), en randomisant diverses choses dans l’espace d’adressage du processus, y compris la position de la stack et sa taille.

La théorie sonne bien. Mettons-le en pratique!

Pour tester cela, j’ai utilisé un outil Microsoft spécifique à la tâche: EMET ou Enhanced Mitigation Experience Toolkit . Il permet de définir l’indicateur ASLR (et beaucoup plus) au niveau du système ou du processus.
(Il y a aussi une alternative de piratage de registre à l’échelle du système que je n’ai pas essayée)

EMET GUI

Afin de vérifier l’efficacité de l’outil, j’ai également découvert que Process Explorer signale correctement le statut de l’indicateur ASLR dans la page “Propriétés” du processus. Jamais vu ça jusqu’à aujourd’hui 🙂

entrer la description de l'image ici

En théorie, EMET peut (re) définir l’indicateur ASLR pour un seul processus. En pratique, cela ne semblait rien changer (voir l’image ci-dessus).

Cependant, j’ai désactivé ASLR pour tout le système et (un redémarrage plus tard), je pouvais enfin vérifier que l’exception SO existait toujours à la même profondeur de stack.

PRIME

ASLR-lié, dans les nouvelles plus anciennes: Comment Chrome a été composé

Je pense que cela peut être ASLR au travail. Vous pouvez désactiver DEP pour tester cette théorie.

Voir ici pour une classe d’utilitaire C # pour vérifier les informations sur la mémoire: https://stackoverflow.com/a/8716410/552139

En passant, avec cet outil, j’ai constaté que la différence entre la taille de stack maximale et minimale est d’environ 2 Ko, ce qui correspond à une demi-page. C’est bizarre

Mise à jour: OK, maintenant je sais que j’ai raison. J’ai suivi la théorie de la demi-page et j’ai trouvé ce document qui examine l’implémentation ASLR sous Windows: http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf

Citation:

Une fois la stack placée, le pointeur de stack initial est ensuite randomisé par une quantité décrémentielle aléatoire. Le décalage initial est sélectionné pour être jusqu’à une demi-page (2 048 octets)

Et c’est la réponse à votre question. ASLR prend au hasard entre 0 et 2048 octets de votre stack initiale.

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

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

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