Didacticiel Ildasm.exe
Ce didacticiel est une introduction au désassembleur MSIL Disassembler (Ildasm.exe) inclus avec le kit de développement .NET Framework SDK. ILDasm.exe analyse toutes sortes d'assemblys .NET Framework .exe ou .dll et présente les informations dans un format explicite. Cet outil affiche bien plus que du code MSIL (Microsoft Intermediate Language) ; il présente également les espaces de noms et les types, interfaces comprises. Vous pouvez l'utiliser pour étudier les assemblys .NET Framework natifs, tels que Mscorlib.dll, ainsi que les assemblys .NET Framework fournis par des tiers ou que vous avez créés vous-même. Ildasm.exe va s'avérer être un outil indispensable pour un grand nombre de développeurs .NET Framework.
Pour ce didacticiel, utilisez la version Visual C# de l'exemple WordCount inclus avec le SDK. Vous pouvez aussi vous servir de la version Visual Basic mais, dans ce cas, le MSIL généré sera différent pour les deux langages et les images à l'écran présenteront quelques divergences. WordCount se trouve dans le répertoire <FrameworkSDK>\Samples\Applications\WordCount\. Pour créer et exécuter l'exemple, suivez les instructions données dans le fichier Readme.htm. Pour étudier l'assembly WordCount.exe, ce didacticiel fait appel à Ildasm.exe.
Pour commencer, créez l'exemple WordCount, puis chargez-le dans Ildasm.exe à l'aide de la ligne de commande suivante :
ildasm WordCount.exe
La fenêtre d'Ildasm.exe s'affiche, comme illustré sur la figure suivante.
L'arborescence de la fenêtre d'Ildasm.exe affiche des informations sur le manifeste d'assembly contenues dans WordCount.exe et dans les quatre types classe globaux, à savoir App, ArgParser, WordCountArgParser et WordCounter.
Si vous double-cliquez sur un type quelconque de l'arborescence, des informations supplémentaires s'affichent sur le type en question. Sur la figure suivante, le type classe WordCounter a été développé.
Sur la figure précédente, vous voyez tous les membres de WordCounter. Le tableau suivant donne la signification de chaque symbole graphique.
Symbole | Signification |
---|---|
Plus d'infos | |
Espace de noms | |
Classe | |
Interface | |
Classe de valeurs | |
Enum | |
Méthode | |
Méthode statique | |
Champ | |
Champ statique | |
Événement | |
Propriété | |
Élément de manifeste ou d'infos de classe |
Si vous double-cliquez sur l'entrée .class public auto ansi beforefieldinit, les informations suivantes s'affichent :
Sur la figure précédente, vous voyez aisément que le type WordCounter est dérivé du type System.Object.
Le type WordCounter en contient un autre, appelé WordOccurrence. Vous pouvez développer le type WordOccurrence pour en afficher ses membres, comme illustré sur la figure ci-dessous.
En examinant l'arborescence, vous constatez que WordOccurrence implémente l'interface System.IComparable et, plus précisément, la méthode CompareTo. La suite de cette conversation va toutefois ignorer le type WordOccurrence et se concentrer à la place sur le type WordCounter.
Le type WordCounter contient cinq champs private : totalBytes, totalChars, totalLines, totalWords et wordCounter. Les quatre premiers sont des instances de type int64, tandis que le dernier est une référence à un type System.Collections.SortedList.
Les méthodes s'affichent à la suite des champs. La première méthode, .ctor, est un constructeur. Ce type particulier ne comporte qu'un constructeur, mais les autres types peuvent en contenir plusieurs, chacun portant sa propre signature. Le constructeur WordCounter comporte un type de retour void (comme tous les constructeurs) et n'accepte aucun paramètre. Si vous double-cliquez sur la méthode du constructeur, une nouvelle fenêtre affiche le code MSIL contenu dans la méthode, comme illustré sur la figure ci-après.
À ce stade, le code MSIL est relativement facile à lire et à comprendre. (Pour une présentation exhaustive, consultez CIL Instruction Set Specification, qui se trouve dans la Partie III du fichier CIL.doc dans le dossier <FrameworkSDK>\Tool Developers Guide\Docs\.) Tout en haut, vous voyez que ce constructeur requiert 50 octets de code MSIL. Ce nombre ne vous donne pas d'informations significatives sur la quantité de code natif émise par le compilateur JIT, puisque la taille dépend du CPU de l'hôte et du compilateur utilisé pour générer le code.
Le Common Language Runtime est fondé sur les piles. C'est pourquoi, quelle que soit l'opération à effectuer, le code MSIL exécute un push sur les opérandes dans une pile virtuelle, puis exécute l'opérateur. Ce dernier retire les opérandes de la pile, effectue l'opération requise, puis replace le résultat dans la pile. En une opération, cette méthode n'a jamais plus de huit opérandes ayant fait l'objet d'un push dans la pile virtuelle. Vous pouvez identifier ce chiffre en regardant l'attribut .maxstack qui apparaît juste avant le code MSIL.
Maintenant, examinez les premières instructions MSIL qui sont reproduites sur les quatre lignes suivantes :
IL_0000: ldarg.0 ; Load the object's 'this' pointer on the stack
IL_0001: ldc.i4.0 ; Load the constant 4-byte value of 0 on the stack
IL_0002: conv.i8 ; Convert the 4-byte 0 to an 8-byte 0
IL_0003: stfld int64 WordCounter::totalLines
L'instruction située à l'emplacement IL_0000 charge le premier paramètre passé à la méthode dans la pile virtuelle. Chaque méthode d'instance passe toujours l'adresse de la mémoire de l'objet. Cet argument, appelé Argument zéro, ne figure jamais de façon explicite dans la signature de la méthode. C'est pourquoi, même si elle semble recevoir des arguments zéro, la méthode .ctor n'en reçoit en fait qu'un seul. Ensuite, l'instruction située à l'emplacement IL_0000 charge le pointeur de cet objet dans la pile virtuelle.
L'instruction située à l'emplacement IL_0001 charge dans la pile virtuelle une valeur zéro constante sur 4 octets.
L'instruction située à l'emplacement IL_0002 prend la valeur du haut de la pile (zéro sur 4 octets) et la convertit en zéro sur 8 octets, plaçant ainsi le zéro de 8 octets en haut de la pile.
À ce stade, la pile contient deux opérandes : le zéro sur 8 octets et le pointeur sur cet objet. L'instruction située à l'emplacement IL_0003 se sert de ces opérandes pour stocker la valeur située en haut de la pile (le zéro sur 8 octets) dans le champ totalLines de l'objet identifié dans la pile.
La même séquence d'instructions MSIL se répète pour les champs totalChars, totalBytes et totalWords.
L'initialisation du champ wordCounter commence par l'instruction IL_0020, comme illustré ci-dessous :
IL_0020: ldarg.0
IL_0021: newobj instance void [mscorlib]System.Collections.SortedList::.ctor()
IL_0026: stfld class [mscorlib]System.Collections.SortedList WordCounter::wordCounter
L'instruction située à l'emplacement IL_0020 exécute un push sur le pointeur this pour ce WordCounter dans la pile virtuelle. Cette opérande n'est pas utilisée par l'instruction newobj mais le sera par l'instruction the stfld qui se trouve à l'emplacement IL_0026.
L'instruction située à l'emplacement IL_0021 demande au runtime de créer un objet System.Collections.SortedList et d'appeler son constructeur sans arguments. Lorsque newobj retourne une valeur, l'adresse de l'objet SortedList se trouve dans la pile. À ce stade, l'instruction stfld située à l'emplacement IL_0026 stocke le pointeur de l'objet SortedList dans le champ wordCounter de l'objet WordCounter.
Une fois que tous les champs de l'objet WordCounter ont été initialisés, l'instruction située à l'emplacement IL_002b exécute un push sur le pointeur this dans la pile virtuelle et IL_002c appelle le constructeur dans le type de base (System.Object).
Naturellement, la dernière instruction située à l'emplacement IL_0031 est l'instruction de retour qui oblige le constructeur WordCounter à retourner au code qui l'a créé. Étant donné que les constructeurs doivent retourner void, rien n'est placé dans la pile avant le retour du constructeur.
Prenez maintenant un autre exemple. Double-cliquez sur la méthode GetWordsByOccurranceEnumerator pour afficher son code MSIL, illustré sur la figure ci-dessous.
Vous voyez que la taille du code pour cette méthode est de 69 octets et que la méthode requiert quatre emplacements dans la pile virtuelle. De plus, la méthode possède trois variables locales : la première a pour type System.Collection.SortedList et les deux autres le type System.Collections.IDictionaryEnumerator. Notez que les noms de variable mentionnés dans le code source ne sont pas envoyés au code source tant que l'assembly n'a pas été compilée avec l'option /debug. Si /debug n'est pas utilisée, les noms de variable V_0, V_1 et V_2 sont respectivement utilisés en lieu et place de sl, de et CS$00000003$00000000.
Au départ, cette méthode commence par exécuter l'instruction newobj qui crée un nouveau System.Collections.SortedList et appelle le constructeur par défaut de cet objet. Lorsque newobj retourne une valeur, l'adresse de l'objet créé se trouve dans la pile virtuelle. L'instruction stloc.0 (IL_0005) stocke cette valeur dans la variable locale 0 ou dans sl (V_0 sans /debug) (qui est de type System.Collections.SortedList).
Avec les instructions situées aux emplacements IL_0006 et IL_0007, le pointeur this de l'objet WordCounter (dans l'argument zéro passé à la méthode) est chargé dans la pile et la méthode GetWordsAlphabeticallyEnumerator est appelée. Lorsque l'instruction call retourne une valeur, l'adresse de l'énumérateur se trouve dans la pile. L'instruction stloc.1 (IL_000c) enregistre cette adresse dans la variable locale 1 ou dans de (V_1 sans /debug) qui est de type System.Collections.IDictionaryEnumerator.
L'instruction br.s située à l'emplacement IL_000d provoque un branchement non conditionnel à la condition de test IL de l'instruction while. Cette condition de test IL commence à l'instruction IL_0032. À l'emplacement IL_0032, l'adresse de de (ou V_1) (le IDictionaryEnumerator) fait l'objet d'un push dans la pile (push) et, à IL_0033, sa méthode MoveNext est appelée. Si MoveNext retourne true, une entrée existe en vue de son énumération et l'instruction brtrue.s passe à l'instruction IL_000f.
Pour les instructions situées à IL_000f et IL_0010, les adresses des objets dans sl (ou V_0) et de (ou V_1) font l'objet d'un push dans la pile. Ensuite, la méthode de la propriété get_Value de l'objet IdictionaryEnumerator est appelée pour obtenir le nombre d'occurrences de l'entrée actuelle. Ce nombre est une valeur sur 32 bits qui est stockée dans un System.Int32. Le code effectue un cast de cet objet Int32 en un type valeur int. Le casting d'un type référence à un type valeur requiert l'instruction unbox à l'emplacement IL_0016. Lorsque unbox retourne une valeur, l'adresse de la valeur unboxed est dans la pile. L'instruction ldind.i4 (IL_001b) charge une valeur sur 4 octets (qui pointe sur l'adresse actuellement dans la pile), dans la pile. Autrement dit, l'entier unboxed sur 4 octets est placé dans la pile.
Pour l'instruction à IL_001c, la valeur de sl (ou de V_1) – l'adresse de IDictionaryEnumerator) – fait l'objet d'un push dans la pile et sa méthode de propriété get_Key est appelée. Lorsque get_Key retourne une valeur, l'adresse de l'objet System.Object se trouve dans la pile. Le code sait que le dictionnaire contient des chaînes, c'est pourquoi le compilateur effectue un cast de cet Object en un String à l'aide de l'instruction castclass qui se trouve à l'emplacement IL_0022.
Les instructions suivantes (allant de IL_0027 à IL_002d) créent un nouvel objet WordOccurrence et passent l'adresse de l'objet à la méthode Add de SortedLists.
Pour l'instruction à IL_0032, la condition de test de l'instruction while est évaluée de nouveau. Si MoveNext retourne true, la boucle exécute une autre itération. Toutefois, si MoveNext retourne false, l'exécution passe dans la boucle et se termine par l'instruction à IL_003a. Les instructions allant de IL_003a à IL_0040 appellent la méthode GetEnumerator de l'objet SortLists. La valeur retournée est System.Collections.IDictionaryEnumerator, qui est conservée dans la pile pour devenir la valeur de retour de GetWordsByOccurrenceEnumerator.