IL2CPP interne : Conseils de débogage pour le code généré

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
May 20, 2015|8 Min
IL2CPP interne : Conseils de débogage pour le code généré
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.

Ceci est le troisième article de blog dans la série IL2CPP Internals. Dans ce billet, nous allons explorer quelques astuces qui rendent le débogage du code C++ généré par IL2CPP un peu plus facile. Nous verrons comment définir des points d'arrêt, visualiser le contenu des chaînes et des types définis par l'utilisateur et déterminer où se produisent les exceptions.

Pour ce faire, considérons que nous déboguons du code C++ généré à partir de code IL .NET. Le débogage ne sera donc probablement pas l'expérience la plus agréable. Cependant, avec quelques-unes de ces astuces, il est possible d'obtenir un aperçu significatif de la façon dont le code d'un projet Unity s'exécute sur l'appareil cible réel (nous parlerons un peu du débogage du code géré à la fin de cet article).

Préparez-vous également à ce que le code généré dans votre projet diffère de ce code. Avec chaque nouvelle version de Unity, nous cherchons à améliorer le code généré, à le rendre plus rapide et plus petit.

La mise en place

Pour cet article, j'utilise Unity 5.0.1p3 sur OSX. Je vais utiliser le même projet d'exemple que dans l'article sur le code généré, mais cette fois je vais construire pour la cible iOS en utilisant le backend de script IL2CPP. Comme je l'ai fait dans l'article précédent, je construirai avec l'option "Development Player" sélectionnée, de sorte que il2cpp.exe génère du code C++ avec des noms de types et de méthodes basés sur les noms du code IL.

Une fois que Unity a fini de générer le projet Xcode, je peux l'ouvrir dans Xcode (j'ai la version 6.3.1, mais n'importe quelle version récente devrait fonctionner), choisir mon appareil cible (un iPad Mini 3, mais n'importe quel appareil iOS devrait fonctionner) et construire le projet dans Xcode.

Définition des points d'arrêt

Avant d'exécuter le projet, je vais d'abord placer un point d'arrêt au début de la méthode Start dans la classe HelloWorld. Comme nous l'avons vu dans l'article précédent, le nom de cette méthode dans le code C++ généré est HelloWorld_Start_m3. Nous pouvons utiliser Cmd+Shift+O et commencer à taper le nom de cette méthode pour la trouver dans Xcode, puis y placer un point d'arrêt.

image05

Nous pouvons également choisir Debug > Breakpoints > Create Symbolic Breakpoint dans XCode, et le configurer pour qu'il s'arrête à cette méthode.

image02

Maintenant, quand je lance le projet Xcode, je vois immédiatement qu'il se casse au début de la méthode.

Nous pouvons définir des points d'arrêt sur d'autres méthodes dans le code généré comme ceci si nous connaissons le nom de la méthode. Nous pouvons également définir des points d'arrêt dans Xcode à une ligne spécifique dans l'un des fichiers de code générés. En fait, tous les fichiers générés font partie du projet Xcode. Vous les trouverez dans le navigateur de projet, dans le répertoire Classes/Native.

image03

Visualisation des chaînes

Il y a deux façons de voir la représentation d'une chaîne IL2CPP dans Xcode. Nous pouvons voir la mémoire d'une chaîne directement, ou nous pouvons appeler l'un des utilitaires de chaîne dans libil2cpp pour convertir la chaîne en une std::chaîne, que Xcode peut afficher. Examinons la valeur de la chaîne nommée _stringLiteral1 (spoiler alert : son contenu est "Hello, IL2CPP !").

Dans le code généré avec Ctags construit (ou en utilisant Cmd+Ctrl+J dans Xcode), nous pouvons sauter à la définition de _stringLiteral1 et voir que son type est Il2CppString_14 :

Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.

En fait, toutes les chaînes de caractères dans IL2CPP sont représentées de cette manière. Vous trouverez la définition de Il2CppString dans le fichier d'en-tête object-internals.h. Ces chaînes comprennent l'en-tête standard de tout type géré dans IL2CPP, Il2CppObject (auquel on accède via le typedef Il2CppDataSegmentString), suivi d'une longueur de quatre octets, puis d'un tableau de caractères de deux octets. Les chaînes définies à la compilation, comme _stringLiteral1, se retrouvent avec un tableau de caractères de longueur fixe, alors que les chaînes créées à l'exécution ont un tableau alloué. Les caractères de la chaîne sont codés en UTF-16.

Si nous ajoutons _stringLiteral1 à la fenêtre de surveillance dans Xcode, nous pouvons sélectionner l'option View Memory of "_stringLiteral1" pour voir la disposition de la chaîne en mémoire.

image06

Ensuite, dans le visualiseur de mémoire, nous pouvons voir ceci :

image00

Le membre d'en-tête de la chaîne est de 16 octets, de sorte qu'après l'avoir passé, nous pouvons voir que les quatre octets de la taille ont une valeur de 0x000E (14). L'octet suivant la longueur est le premier caractère de la chaîne, 0x0048 ('H'). Comme chaque caractère a une largeur de deux octets, mais que dans cette chaîne tous les caractères tiennent dans un seul octet, Xcode les affiche à droite avec des points entre chaque caractère. Toutefois, le contenu de la chaîne est clairement visible. Cette méthode de visualisation des chaînes fonctionne, mais elle est un peu difficile pour les chaînes plus complexes.

Nous pouvons également visualiser le contenu d'une chaîne à partir de l'invite lldb dans Xcode. L'en-tête utils/StringUtils.h nous donne l'interface de certains utilitaires de chaînes de caractères de libil2cpp que nous pouvons utiliser. Plus précisément, appelons la méthode Utf16ToUtf8 à partir de l'invite lldb. Son interface se présente comme suit :

Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.

Nous pouvons passer le membre chars de la structure C++ à cette méthode, qui renverra une std::string encodée en UTF-8. Ensuite, à l'invite de lldb, si nous utilisons la commande p, nous pouvons imprimer le contenu de la chaîne.

Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.


Visualisation des types définis par l'utilisateur

Nous pouvons également visualiser le contenu d'un type défini par l'utilisateur. Dans le code du script simple de ce projet, nous avons créé un type C# nommé Important avec un champ nommé InstanceIdentifier. Si je place un point d'arrêt juste après la création de la deuxième instance du type Important dans le script, je peux voir que le code généré a fixé InstanceIdentifier à la valeur 1, comme prévu.

image09

Ainsi, l'affichage du contenu des types définis par l'utilisateur dans le code généré se fait de la même manière que vous le feriez normalement dans du code C++ dans Xcode.

Rupture des exceptions dans le code généré

Il m'arrive souvent de déboguer le code généré pour essayer de trouver la cause d'un bogue. Dans de nombreux cas, ces bogues se manifestent sous la forme d'exceptions gérées. Comme nous l'avons vu dans le dernier article, IL2CPP utilise les exceptions C++ pour implémenter les exceptions gérées, de sorte que nous pouvons casser lorsqu'une exception gérée se produit dans Xcode de plusieurs façons.

La manière la plus simple d'interrompre une exception gérée est de placer un point d'arrêt sur la fonction il2cpp_codegen_raise_exception, qui est utilisée par il2cpp.exe à chaque fois qu'une exception gérée est explicitement levée.

image08

Si je laisse ensuite le projet s'exécuter, Xcode s'interrompt lorsque le code dans Start lance une exception InvalidOperationException. C'est ici que l'affichage du contenu des chaînes de caractères peut s'avérer très utile. Si j'examine les membres de l'argument ex, je constate qu'il possède un membre ___message_2, qui est une chaîne de caractères représentant le message de l'exception.

image07

Avec un peu de doigté, nous pouvons imprimer la valeur de cette chaîne et voir quel est le problème :

Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.


Notez que la chaîne a la même présentation que ci-dessus, mais que les noms des champs générés sont légèrement différents. Le champ chars s'appelle ___start_char_1 et son type est uint16_t, et non uint16_t[]. Il s'agit toujours du premier caractère d'un tableau, nous pouvons donc passer son adresse à la fonction de conversion, et nous constatons que le message de cette exception est plutôt réconfortant.

Mais toutes les exceptions gérées ne sont pas explicitement levées par le code généré. Le code d'exécution de libil2cpp lèvera des exceptions gérées dans certains cas et n'appellera pas il2cpp_codegen_raise_exception pour le faire. Comment rattraper ces exceptions ?

Si nous utilisons Debug > Breakpoints > Create Exception Breakpoint dans Xcode, puis éditons le point d'arrêt, nous pouvons choisir les exceptions C++ et nous arrêter lorsqu'une exception de type Il2CppExceptionWrapper est levée. Comme ce type C++ est utilisé pour envelopper toutes les exceptions gérées, il nous permettra d'attraper toutes les exceptions gérées.

image10

Prouvons que cela fonctionne en ajoutant les deux lignes de code suivantes au début de la méthode Start de notre script :

Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.

La deuxième ligne provoque une exception de type NullReferenceException. Si nous exécutons ce code dans Xcode avec le point d'arrêt d'exception défini, nous verrons que Xcode s'arrêtera effectivement lorsque l'exception sera levée. Cependant, le point d'arrêt se trouve dans le code de libil2cpp, de sorte que tout ce que nous voyons est du code assembleur. Si nous examinons la pile d'appels, nous constatons que nous devons remonter de quelques cadres jusqu'à la méthode NullCheck, qui est injectée par il2cpp.exe dans le code généré.

image01

À partir de là, nous pouvons remonter d'un cadre supplémentaire et constater que notre instance du type Important a bien la valeur NULL.

image04

Conclusion

Après avoir discuté de quelques astuces pour déboguer le code généré, j'espère que vous avez une meilleure compréhension de la manière de traquer les problèmes possibles en utilisant le code C++ généré par IL2CPP. Je vous encourage à étudier la disposition des autres types utilisés par IL2CPP pour en savoir plus sur la manière de déboguer le code généré.

Mais où se trouve le débogueur de code géré IL2CPP ? Ne devrions-nous pas être en mesure de déboguer le code géré exécuté via le backend de script IL2CPP sur un appareil ? En fait, c'est possible. Nous disposons désormais d'un débogueur interne de code géré de qualité alpha pour IL2CPP. Il n'est pas encore prêt à être publié, mais il figure sur notre feuille de route, alors restez à l'écoute.

Le prochain article de cette série examinera les différentes façons dont le backend de script IL2CPP met en œuvre divers types d'invocations de méthodes présentes dans le code géré. Nous allons examiner le coût d'exécution de chaque type d'invocation de méthode.