Que recherchez-vous ?
Technology

La chasse à l'éléphant peu commun

RENÉ DAMM / UNITY TECHNOLOGIESContributor
Apr 22, 2014|5 Min
La chasse à l'éléphant peu commun
Cette page a été traduite automatiquement pour faciliter votre expérience. Nous ne pouvons pas garantir l'exactitude ou la fiabilité du contenu traduit. Si vous avez des doutes quant à la qualité de cette traduction, reportez-vous à la version anglaise de la page web.

La chasse aux insectes est amusante. Et de temps en temps, on s'en sort vivant avec une histoire à raconter à ses petits-enfants ("De mon temps, on chassait encore les insectes avec des bâtons et des pierres" et tout le reste).

La GDC 2014 nous a réservé un autre safari de chasse digne d'un trophée. Nous étions à cinq jours de présenter Unity 5 au monde entier lorsque nous avons "repéré" (enfin, c'était un peu difficile à manquer) un vilain petit éléphant de bug : notre tout nouvel éditeur 64 bits plantait aléatoirement sur OSX au point d'être complètement inutilisable. Il n'y a rien de tel que d'être sur scène pour montrer à quel point votre journaliste est génial toutes les deux minutes.

Alors, Levi, Jonathan et moi avons laissé tomber tous les trucs géniaux sur lesquels nous travaillons (plus d'histoires avec lesquelles nous voulons ennuyer nos petits-enfants) et nous sommes allés faire du repérage. Tout ce que nous savions à ce moment-là, c'est qu'il s'était écrasé quelque part dans le code natif que Mono génère au moment de l'exécution.

Comme tout programmeur le sait, lorsqu'on est confronté à un bogue qui n'est pas évident, il suffit de commencer par rassembler des preuves. Une fois que vous aurez appris suffisamment de choses sur le comportement de l'insecte, vous finirez par pouvoir l'attaquer. Et comme l'heure tournait, nous étions prêts à tirer sur à peu près n'importe quoi.

Mais nous sommes restés sur notre faim. Pour un éléphant, la bestiole s'est révélée étonnamment agile et sournoise.

Cela semble se produire uniquement sous OSX 10.9, bien que Kim ait vu quelque chose de très similaire sous Windows avec sa branche de débogage de la mémoire. Et si vous avez activé Guard Malloc sur les versions antérieures d'OSX, vous avez obtenu un résultat assez similaire. Cependant, comme il se plantait dans un code de script aléatoire à des profondeurs arbitraires dans la hiérarchie des appels, il était difficile de dire avec certitude ce qui était le même plantage et ce qui ne l'était pas. L'accident peut être constant pendant dix séries consécutives, puis totalement différent pendant les cinq suivantes.

Ainsi, pendant que Kim et moi nous enfoncions dans la mémoire jusqu'aux genoux et dans le code assembleur jusqu'aux cuisses, Levi lançait une trace exhaustive de toutes les activités secrètes et moins secrètes de Mono pour générer un journal de plusieurs gigaoctets et un éditeur qui fonctionnait à la vitesse de ma grand-mère. C'est ce qui a donné lieu à la première constatation intéressante : apparemment, nous compilions toujours la méthode dans laquelle nous nous sommes écrasés juste avant que les choses ne tournent mal.

Mais qu'est-ce qui a provoqué l'accident ? La cause immédiate était que nous essayions d'exécuter du code à partir d'une adresse non valide. Comment en sommes-nous arrivés là ? Un bogue dans la gestion des signaux par Mono qui ne permet pas de reprendre correctement ? Un bogue dans le compilateur JIT de Mono qui ne permet pas de revenir correctement au code compilé ? Un autre thread corrompt la mémoire de la pile du thread principal ? Les fées et les grincheux ? (pendant un moment, cette dernière hypothèse a semblé la plus probable).

Après deux jours de chasse, l'éléphant était encore bien vivant et se déplaçait.

Samedi soir, je me suis donc équipée d'un carnet, de quatre stylos de couleurs différentes et d'une grande quantité de bière provenant de notre réfrigérateur Unity (en veillant à ne pas toucher à l'horrible bière de Noël en boîte que nous avons encore coincée dans ses crevasses). J'ai ensuite fait tourner les instances d'Unity jusqu'à ce que quatre crashs différents soient figés dans le débogueur, je les ai étiquetés "Red Crash", "Blue Crash", "Green Crash" et "Black Crash" et je me suis mis au travail avec mes stylos de couleur respectifs pour prendre des notes et dessiner des diagrammes pas si jolis que ça de tout ce que j'ai trouvé.

Voici mes notes pour Blue Crash :

Et c'est là que j'ai fait ma première découverte : dans tous les cas, la pile était plus grande de 16 octets qu'elle ne devrait l'être !

Cela a conduit à la découverte suivante : pour tous les crashs, l'examen de ces 16 octets supplémentaires a permis de trouver une adresse de retour dans la fonction dans laquelle le crash s'est produit. Une trace montrait clairement que, dans tous les cas, nous avions déjà exécuté quelques appels de la même méthode, et j'ai d'abord pensé que l'adresse provenait du dernier appel que nous avions retracé. Cependant, un examen plus approfondi a révélé qu'il s'agissait en fait de l'adresse de retour d'un appel dont la méthode n'avait pas encore été compilée !

Cela m'a laissé perplexe pendant un moment car, dans certains cas, il y avait plusieurs appels entre la dernière méthode tracée et cet appel qui n'avaient pas encore été compilés non plus. En y regardant de plus près, on s'aperçoit que nous avons toujours sauté autour d'eux.

Alors, j'ai regardé cette fonction dont nous étions apparemment censés revenir...

Et voilà le résultat (surligné en bleu) : Nous avons sauté dans la mauvaise direction !

Ce que Mono fait ici, c'est créer de petites fonctions "trampoline" qui ne contiennent qu'un appel au compilateur JIT et quelques données encodées dans le flux d'instructions après l'appel (utilisées par le compilateur JIT pour savoir quelle méthode compiler). Une fois que le compilateur JIT aura fait son travail, il supprimera ces trampolines et effacera toute trace de l'appel de la méthode.

Cependant, l'instruction d'appel que vous voyez ici est ce que l'on appelle un "appel proche" qui, incidemment, utilise un décalage signé de 32 bits pour sauter par rapport à l'instruction suivante.

Et comme un nombre signé sur 32 bits ne peut atteindre que 2 Go de haut en bas et que nous fonctionnons en 64 bits, nous avons soudain compris pourquoi la disposition de la mémoire du tas jouait un rôle si crucial dans la reproduction du bogue : une fois que les trampolines de Mono étaient éloignés de plus de 2 Go du compilateur JIT, les offsets ne tenaient plus sur 32 bits et étaient tronqués lors de l'émission de l'instruction d'appel.

À ce moment-là, Jonathan a rapidement trouvé le bon correctif et, une fois son dimanche terminé, nous avions une version stable et prête à temps pour la GDC.

Vous connaissez tous l'histoire de ce pays. Nous avons présenté avec succès Unity 5 à la GDC 2014, qui a suscité des critiques dithyrambiques, et après son lancement, il est rapidement devenu le logiciel le plus aimé de tous les temps. Oh, attendez, ce n'est pas encore le cas...

Avant ce lancement, il y a encore beaucoup de crashs noirs et bleus à corriger :).