Décompiler les enums [Flags] : comment Glass.NET retransforme ldc.i4.3 en Read | Write
Une combinaison de drapeaux comme Read | Write est repliée en un seul littéral entier avant même d'atteindre l'IL, si bien que le | que vous avez écrit disparaît. Cette analyse lit l'IL réel du code à drapeaux, explique les signaux de métadonnées de FlagsAttribute et des constantes nommées qui permettent à un décompilateur de décomposer un entier brut en une somme de membres nommés, et montre comment Glass.NET reconstruit les motifs | & ~ et HasFlag au lieu de vous laisser (Permissions)3.
Un enum [Flags] est l’un des idiomes les plus anciens de .NET, et l’un des plus trompeurs à relire depuis un assembly compilé. Vous écrivez quelque chose d’expressif — Permissions.Read | Permissions.Write — et vous attendez raisonnablement que le décompilateur vous montre exactement cela. Mais au moment où votre code atteint l’IL, le | a souvent disparu, les deux noms de membre ont disparu, et tout ce qui subsiste est un littéral entier nu. Que vous récupériez Read | Write ou un plat (Permissions)3 dépend entièrement de la qualité avec laquelle le décompilateur lit une poignée de signaux de métadonnées. Cet article lit l’IL réel et montre comment Glass.NET, le décompilateur .NET gratuit, reconstruit toute la famille des opérations à drapeaux.
Ce qu’est réellement un enum dans les métadonnées
Un enum n’est pas une construction spéciale du runtime. C’est un type valeur qui dérive de System.Enum, porte un type entier sous-jacent (int par défaut) et liste ses membres comme des champs static literal nommés. Observons :
[Flags]
public enum Permissions
{
None = 0,
Read = 1,
Write = 2,
Execute = 4,
All = Read | Write | Execute // = 7
}
Pointez Glass vers le type et demandez sa forme :
glass decompile Acme.Storage.dll --type Acme.Storage.Permissions
Ce que Glass lit dans les métadonnées, ce sont quatre choses : le type dérive de System.Enum ; il a un champ d’instance au nom spécial value__ de type int (c’est là qu’est enregistré le type sous-jacent) ; il a cinq champs static literal nommés, chacun avec une valeur constante dans le tas #Blob ; et il porte un attribut personnalisé System.FlagsAttribute. Ce sont les seules entrées. Il n’y a nulle part de « ceci est Read-ou-Write » stocké — la combinaison All = 7 n’est qu’un autre champ constant. Tout le reste est reconstruction.
Le OU qui s’évanouit
Voici maintenant la partie qui piège les outils naïfs. Voici une méthode qui ouvre un fichier avec deux drapeaux :
public Stream Open(string path) =>
_fs.Open(path, Permissions.Read | Permissions.Write);
Demandez l’IL à Glass :
glass il Acme.Storage.dll --method Acme.Storage.FileService.Open
Les instructions pertinentes sont :
ldarg.0
ldfld class IFileSystem Acme.Storage.FileService::_fs
ldarg.1
ldc.i4.3
callvirt instance class Stream IFileSystem::Open(string, valuetype Permissions)
Il n’y a aucune instruction or. Permissions.Read et Permissions.Write sont tous deux des constantes de compilation (1 et 2), donc le compilateur C# a évalué 1 | 2 pendant la compilation et émis un seul ldc.i4.3. Le OU binaire que vous avez écrit a été exécuté par le compilateur, pas par le programme. C’est le problème central : l’opérateur source et les noms des membres ne sont pas du tout dans l’IL. Le seul indice que 3 est autre chose qu’un entier ordinaire est la signature de Open, dont le dernier paramètre est typé valuetype Permissions.
Ce contexte de type est l’hameçon. Glass voit une constante entière s’écouler vers un paramètre (ou une variable locale, ou un champ) de type enum, et passe de « afficher le nombre » à « décomposer le nombre contre les membres de cet enum ».
La règle de décomposition, et là où elle devient subtile
Glass décompose une valeur constante en parcourant les membres définis de l’enum et en appariant les bits. La règle doit gérer trois cas qu’une approche en une ligne du type « OU des membres » traite mal.
Zéro. La valeur 0 se reconstruit en None s’il existe un membre de valeur 0, sinon en (Permissions)0. Elle ne se reconstruit jamais en un OU vide.
Membres combinés. La valeur 7 pourrait s’afficher en Read | Write | Execute, mais l’enum définit aussi All = 7. Un bon décompilateur préfère le membre qui correspond exactement : Glass émet Permissions.All, car c’est presque certainement ce que l’auteur a référencé, et c’est une écriture légale et plus lisible de la même valeur. La décomposition ne recourt à joindre les bits individuels par OU que lorsqu’aucun membre nommé ne correspond à la valeur complète.
Bits résiduels. La valeur 9 active le bit 0 (Read) et le bit 3, qu’aucun membre ne définit. Glass émet Permissions.Read | (Permissions)8 — il nomme chaque bit qu’il peut et caste le reste, de sorte que la reconstruction produit toujours exactement 9. Il ne supprime jamais silencieusement le 8 et n’invente jamais de membre pour lui.
C’est pourquoi [Flags] compte tant pour la sortie. Sans l’attribut, Glass ne décomposera pas du tout — il traite l’enum comme un simple ensemble de valeurs discrètes et affiche (Permissions)3, car un enum qui n’est pas à drapeaux n’offre aucune garantie que 3 soit une combinaison significative plutôt qu’une valeur indéfinie. L’attribut est le seul bit de métadonnées qui transforme un cast plat en code source lisible à drapeaux.
Combinaisons, tests et effacements à l’exécution
Lorsque les opérandes ne sont pas tous deux constants, les opérateurs survivent jusqu’à l’IL et Glass les relit directement.
Un OU à l’exécution — current | extra où les deux sont des locales — compile en une véritable instruction or, et Glass reconstruit current | extra.
Un test de drapeau écrit à la main :
if ((perm & Permissions.Write) != 0) { ... }
compile en ldarg, ldc.i4.2, and, puis un branchement sur le résultat. Glass voit le and contre une constante à membre unique suivi d’une comparaison avec non-zéro et reconstruit le test binaire exact — il ne le réécrit pas en HasFlag.
Un test de drapeau écrit avec l’auxiliaire :
if (perm.HasFlag(Permissions.Write)) { ... }
compile en un box Permissions de l’argument et un callvirt vers System.Enum::HasFlag. Ce site d’appel est reconnaissable sans ambiguïté, donc Glass reproduit perm.HasFlag(Permissions.Write) tel quel. Garder les deux formes distinctes compte : elles se comportent de façon identique pour un seul drapeau mais diffèrent avec des arguments à drapeaux combinés, et quand vous auditez du code inconnu vous voulez lire ce que l’auteur a réellement écrit, et non une réécriture normalisée.
Effacer un drapeau — perm & ~Permissions.Write — compile la constante ~2 en 0xFFFFFFFD repliée à la compilation, puis un and. Ici Glass reconnaît le masque tous-les-bits-sauf-un contre la largeur de l’enum et reconstruit perm & ~Permissions.Write plutôt que perm & (Permissions)4294967293, qui est correct mais illisible.
Le voir de bout en bout
Décompilez la méthode entière et vous récupérez la forme lisible :
glass decompile Acme.Storage.dll --method Acme.Storage.FileService.Configure
public void Configure(Permissions baseline)
{
Permissions effective = baseline | Permissions.Read; // or à l'exécution
if (effective.HasFlag(Permissions.Execute)) // appel à Enum.HasFlag
effective &= ~Permissions.Write; // and avec ~masque replié
_current = effective;
}
Chaque opérateur ici a été récupéré depuis un signal IL différent : le or depuis un véritable opcode or, le HasFlag depuis l’appel virtuel boxé, l’effacement depuis un and contre un complément replié. Rien de tout cela n’était stocké comme code source. C’est tout le travail d’un décompilateur — lire les signaux que le compilateur a laissés derrière lui et reconstruire l’intention par-dessus.
Pourquoi cela vaut la peine d’être compris
Les enums à drapeaux apparaissent partout dans les assemblys réels : modes d’accès aux fichiers, binding flags, options de regex, paramètres de sérialisation, vos propres interrupteurs de fonctionnalités. Quand vous faites un travail légitime — déboguer une bibliothèque dont vous n’avez pas le code, auditer un paquet NuGet avant de lui faire confiance, apprendre comment un framework câble ses options, ou vérifier des frontières d’interop — un décompilateur qui aplatit silencieusement Read | Write en 3 vous coûte la seule chose que vous êtes venu chercher : l’intention de l’auteur. Glass.NET est gratuit, vous pouvez donc lire n’importe quel assembly chargé de drapeaux de cette manière sans barrière de licence, et comme la reconstruction s’appuie sur les métadonnées et l’IL réels, vous pouvez avoir confiance que Read | Write est vraiment 3 et qu’un | (Permissions)8 à la fin signifie qu’il y avait vraiment un bit que personne n’a nommé.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.