Skip to content
← Tous les articles
· Delta1 Labs ILGlass

Lire la vue IL et naviguer dans les tokens de métadonnées dans Glass

Une explication approfondie sur le passage à la vue IL dans Glass, le décodage des tokens de métadonnées (0x06 MethodDef, 0x0A MemberRef, 0x70 UserString), la lecture de l'en-tête du corps d'une méthode et le clic sur un token pour sauter à sa définition.

Le C# qu’un décompilateur vous montre est une reconstruction. Elle est généralement fidèle, mais c’est une interprétation de quelque chose de plus précis : le langage intermédiaire (IL) que le compilateur a réellement émis, plus les tables de métadonnées qui donnent leur sens à chaque nom, type et signature. Quand la reconstruction est propre, vous n’avez jamais besoin de regarder en dessous. Quand elle est ambiguë — une surcharge que vous n’arrivez pas à fixer, une conversion de boxing qui a disparu, une boucle dont la forme ne correspond pas au code attendu — la vue IL est la vérité de référence, et les tokens de métadonnées sont la façon d’y naviguer.

Cet article est une visite de la vue IL de Glass.NET, le décompilateur .NET gratuit. Nous prendrons une petite méthode C#, regarderons son IL, décoderons les tokens de métadonnées que portent les instructions et verrons comment, en cliquant sur un token dans Glass, on saute au membre qu’il nomme. Le cadre, du début à la fin, est légitime : comprendre votre propre code, auditer une dépendance que vous distribuez ou traiter un incident sur un assembly dont vous êtes responsable.

Passer de la vue C# à la vue IL

Ouvrez un assembly dans Glass et sélectionnez une méthode. Par défaut, vous voyez du C# reconstruit. Basculez la vue sur IL (le commutateur de vue du panneau de code) et la même méthode se réaffiche sous forme du bytecode que le JIT consommera. Rien dans l’assembly ne change ; vous regardez le même corps de méthode à travers une lentille de plus bas niveau.

Depuis la CLI, l’équivalent est une option de decompile :

# C# reconstruit (par défaut)
glass decompile MyApp.dll --type MyApp.Billing.Invoice --member Total

# Le même corps de méthode en IL, avec les tokens de métadonnées affichés
glass decompile MyApp.dll --type MyApp.Billing.Invoice --member Total --il

La sortie de --il imprime chaque instruction avec son offset, son opcode et son opérande. Lorsqu’un opérande est un token de métadonnées, Glass imprime la valeur brute de 4 octets et l’annote avec le membre auquel elle se résout, de sorte que vous pouvez lire le corps sans croiser les tables à la main.

Ce qu’est réellement un token de métadonnées

Chaque assembly porte un ensemble de tables de métadonnées — MethodDef, TypeDef, MemberRef, Field, etc. — chacune étant une liste de lignes. Un token de métadonnées est simplement une poignée de 4 octets qui nomme une ligne :

  • L’octet de poids fort est l’id de la table.
  • Les trois octets de poids faible sont l’indice de ligne (à partir de 1) dans cette table.

Les ids de table que vous rencontrerez constamment :

Id de tableTableCe qu’elle nomme
0x02TypeDefUn type défini dans cet assembly
0x01TypeRefUn type défini ailleurs, référencé ici
0x06MethodDefUne méthode définie dans cet assembly
0x0AMemberRefUne méthode ou un champ d’un type externe
0x04FieldUn champ défini dans cet assembly
0x1BTypeSpecUn type construit (p. ex. List<int>)
0x11StandAloneSigUne signature autonome (variables locales)
0x70UserStringUn littéral de chaîne, dans le heap #US

Ainsi, le token 0x0A000012 se décode comme suit : octet de poids fort 0x0A → table MemberRef ; trois octets de poids faible 0x000012 → ligne 18. Il nomme le dix-huitième MemberRef : une méthode ou un champ d’un type d’un autre assembly. Le token 0x06000004 est la ligne 4 de MethodDef : la quatrième méthode définie dans l’assembly que vous regardez. Et 0x70000001 est un token vers le heap UserString à l’offset 1, pas du tout une ligne de table ; plus de détails ci-dessous.

Comment les instructions IL portent des tokens

Prenez cette méthode ordinaire :

public string Describe(Customer c)
{
    var sb = new StringBuilder();
    sb.Append("Customer: ");
    sb.Append(c.Name);
    return sb.ToString();
}

Dans la vue IL, Glass montre quelque chose comme ceci (tokens annotés dans la colonne de droite) :

.method public instance string Describe(class Customer c) cil managed
{
  .maxstack 2
  .locals init (class [System.Runtime]System.Text.StringBuilder V_0)

  IL_0000:  newobj     instance void [System.Runtime]System.Text.StringBuilder::.ctor()   // 0x0A000011  MemberRef
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  ldstr      "Customer: "                                                        // 0x70000001  UserString
  IL_000c:  callvirt   instance class ...StringBuilder StringBuilder::Append(string)       // 0x0A000012  MemberRef
  IL_0011:  pop
  IL_0012:  ldloc.0
  IL_0013:  ldarg.1
  IL_0014:  callvirt   instance string Customer::get_Name()                                // 0x06000007  MethodDef
  IL_0019:  callvirt   instance class ...StringBuilder StringBuilder::Append(string)       // 0x0A000012  MemberRef
  IL_001e:  pop
  IL_001f:  ldloc.0
  IL_0020:  callvirt   instance string StringBuilder::ToString()                           // 0x0A000013  MemberRef
  IL_0025:  ret
}

Lisez les opérandes comme des tokens et les dépendances de la méthode deviennent explicites :

  • newobj 0x0A000011 — construire via un MemberRef : le constructeur de StringBuilder, qui vit dans un autre assembly.
  • ldstr 0x70000001 — empiler un littéral de chaîne. L’opérande est un token UserString ; le texte du littéral vit dans le heap #US.
  • callvirt 0x0A000012 — appeler StringBuilder.Append(string) via un MemberRef. Remarquez que le même token apparaît deux fois : les deux appels à Append se lient à l’unique surcharge Append(string), ce qui est exactement le genre de chose que l’IL rend sans ambiguïté.
  • callvirt 0x06000007 — appeler Customer.get_Name() via un MethodDef, car Customer est défini dans cet assembly, de sorte que l’accesseur de sa propriété est une méthode locale, et non une référence.

Les autres instructions porteuses de tokens suivent la même règle : ldfld/stfld portent un token Field (0x04…) ou MemberRef, ldtoken porte la poignée que vous avez demandé à typeof/fieldof de capturer, castclass et box portent un token de type (0x01…, 0x02… ou 0x1B…), et ldsfld sur un statique externe porte un MemberRef.

Résoudre un token et sauter à sa cible

C’est ici que la vue IL devient un navigateur plutôt qu’une simple liste. Lorsque Glass affiche callvirt 0x0A000012, il fait ce que fait le runtime : il scinde le token en l’id de table 0x0A et la ligne 0x12, cherche la ligne 18 de la table MemberRef et résout le parent, le nom et la signature : StringBuilder.Append(string). Dans l’interface, le membre résolu est un lien. Cliquez et Glass navigue jusqu’à la définition de ce membre (en suivant le MemberRef vers l’assembly référencé s’il est chargé, ou en montrant la signature externe sinon). Un token MethodDef comme 0x06000007 se résout dans l’assembly courant, donc en cliquant vous sautez directement à get_Name dans le même arbre.

callvirt 0x0A000012 scinder le token de 4 octets octet fort 0x0A MemberRef 3 octets faibles 0x000012 ligne 18 table MemberRef, ligne 18 StringBuilder::Append (string) clic → sauter à la définition ldstr 0x70000001 token vers le heap #US → littéral "Customer: " heaps #Strings #US #Blob #GUID

Les heaps : où vivent les chaînes et les blobs

Les tables ne sont pas toute l’histoire. À côté d’elles, les métadonnées comportent quatre heaps :

  • #Strings — les noms des identifiants (noms de types, de méthodes, espaces de noms). Les lignes des tables stockent des offsets dans ce heap, et non le texte lui-même.
  • #US (UserString) — les littéraux de chaîne qu’utilise votre code. L’opérande d’un ldstr est un token 0x70… dont les octets faibles sont un offset dans #US.
  • #Blob — les blobs binaires : signatures, valeurs constantes, arguments d’attributs personnalisés.
  • #GUID — le MVID de l’assembly et d’autres GUID.

Ainsi ldstr 0x70000001 se résout différemment d’un token de table : Glass lit l’offset 0x000001 dans le heap #US et en extrait le littéral — "Customer: " — et l’affiche en ligne. (Si vous voulez ancrer cela dans du code, System.Reflection.Metadata le modélise directement : MetadataReader.GetUserString(UserStringHandle) renvoie exactement ce littéral, et MetadataReader.GetBlobReader vous donne un accès brut au heap #Blob. MetadataTokens.EntityHandle(int token) transforme un token brut en une poignée typée, ce qui est en substance la scission qu’effectue Glass.)

Lire l’en-tête du corps de la méthode

Au-dessus des instructions, tout corps de méthode commence par un en-tête, et la vue IL le reflète. Il y a deux formats :

  • Format tiny. Utilisé lorsque la méthode n’a pas de locaux, pas de gestionnaires d’exceptions et une profondeur de pile de 8 ou moins. Tout l’en-tête tient en un seul octet : les deux bits de poids faible marquent « tiny » et les six bits de poids fort contiennent la taille du code. Il n’y a rien d’autre à lire.
  • Format fat. Utilisé pour tout le reste. Un en-tête de 12 octets porte les flags, la profondeur de max stack, la taille du code et un token de signature des variables locales.

Ce token de signature des locaux est lui-même un token de métadonnées vers la table StandAloneSig (0x11…). Il ne nomme ni une méthode ni un type : il pointe vers un blob décrivant les locaux de la méthode. Dans notre exemple Describe, la ligne .locals init (…StringBuilder V_0) et la ligne .maxstack 2 sortent toutes deux directement de l’en-tête fat ; le type de l’unique local a été récupéré en résolvant le token StandAloneSig vers son blob de signature. Quand vous voyez .maxstack et une liste .locals dans la vue IL, vous lisez un en-tête fat décodé.

Ce que la vue IL montre et que le C# cache

La raison de descendre à l’IL est que le C# est avec perte dans le sens de la reconstruction. Quelques points qu’il lisse systématiquement :

  • Boxing explicite. object o = 42; disparaît dans une affectation en C#, mais l’IL montre box [System.Runtime]System.Int32 avec un token de type. Si vous traquez une allocation ou une InvalidCastException surprenante, les instructions box/unbox.any ne sont visibles qu’ici.
  • Structure des branches. Un foreach, un while, un switch avec filtrage par motif et un ternaire peuvent tous se reconstruire en du C# d’aspect similaire, mais l’IL montre les instructions exactes br, brtrue, blt et switch et leurs offsets cibles. Quand une boucle reconstruite paraît fausse, lisez les branches.
  • La surcharge exacte à laquelle on se lie. En C#, Append(...) pourrait être l’une d’une dizaine de surcharges de StringBuilder.Append. En IL, le token de call/callvirt se résout en une seule signature. Décodez-le et vous saurez précisément quelle surcharge le compilateur a choisie : l’ambiguïté a disparu.

Voilà le bénéfice pratique. Quand la reconstruction C# de Glass est sûre d’elle, faites-lui confiance. Quand elle est ambiguë — des génériques qui n’ont pas fait un aller-retour propre, une build de release optimisée, un assembly dont les noms ont été supprimés — basculez en IL, lisez les tokens et cliquez pour résoudre ce que la vue de plus haut niveau a dû deviner. L’IL plus les métadonnées est la vérité de référence de l’assembly ; la vue C# est la commodité construite par-dessus.

Et ensuite

Ouvrez dans Glass un assembly qui vous appartient, choisissez n’importe quelle méthode avec quelques appels externes et basculez en vue IL. Décodez un token de call à la main — scindez l’octet de poids fort, trouvez la table, comptez jusqu’à la ligne — puis cliquez pour confirmer que Glass vous amène au même membre. Une fois cet aller-retour devenu naturel, la vue IL cesse d’intimider et devient le moyen le plus rapide de répondre à « que fait ceci réellement ? » lorsque le C# ne le dit pas. Glass est gratuit ; téléchargez-le depuis la page du produit et lisez vos binaires jusqu’au bout.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.