Skip to content
← Alle Beiträge
· Delta1 Labs DecompilationGlass

[Flags]-Enums dekompilieren: wie Glass.NET aus ldc.i4.3 wieder Read | Write macht

Eine Flag-Kombination wie Read | Write wird schon vor dem IL zu einem einzigen Integer-Literal zusammengefaltet, sodass das von dir geschriebene | verschwindet. Diese Analyse liest den echten IL von Flag-Code, erklärt die Metadaten-Signale von FlagsAttribute und benannten Konstanten, mit denen ein Dekompiler einen rohen Integer in eine Summe benannter Member zerlegen kann, und zeigt, wie Glass.NET die Muster | & ~ und HasFlag rekonstruiert, statt dir (Permissions)3 zu hinterlassen.

Ein [Flags]-Enum gehört zu den ältesten Redewendungen in .NET und zu den irreführendsten, wenn man sie aus einem kompilierten Assembly zurücklesen will. Du schreibst etwas Ausdrucksstarkes — Permissions.Read | Permissions.Write — und erwartest zu Recht, dass der Dekompiler dir genau das zeigt. Doch wenn dein Code im IL ankommt, ist das | oft weg, die beiden Member-Namen sind weg, und alles, was übrig bleibt, ist ein nacktes Integer-Literal. Ob du Read | Write zurückbekommst oder ein flaches (Permissions)3, hängt ganz davon ab, wie gut der Dekompiler eine Handvoll Metadaten-Signale liest. Dieser Beitrag liest den echten IL und zeigt, wie Glass.NET, der kostenlose .NET-Dekompiler, die gesamte Familie der Flag-Operationen rekonstruiert.

Was ein Enum in den Metadaten tatsächlich ist

Ein Enum ist kein spezielles Runtime-Konstrukt. Es ist ein Werttyp, der von System.Enum abgeleitet ist, einen zugrunde liegenden Integer-Typ trägt (standardmäßig int) und seine Member als benannte static literal-Felder auflistet. Betrachte:

[Flags]
public enum Permissions
{
    None    = 0,
    Read    = 1,
    Write   = 2,
    Execute = 4,
    All     = Read | Write | Execute   // = 7
}

Richte Glass auf den Typ und frage nach seiner Form:

glass decompile Acme.Storage.dll --type Acme.Storage.Permissions

Was Glass aus den Metadaten liest, sind vier Dinge: Der Typ leitet von System.Enum ab; er hat ein Instanzfeld mit dem speziellen Namen value__ vom Typ int (dort wird der zugrunde liegende Typ festgehalten); er hat fünf benannte static literal-Felder, jedes mit einem konstanten Wert im #Blob-Heap; und er trägt ein benutzerdefiniertes Attribut System.FlagsAttribute. Das sind die einzigen Eingaben. Nirgends ist ein „das ist Read-oder-Write” gespeichert — die Kombination All = 7 ist einfach ein weiteres konstantes Feld. Alles andere ist Rekonstruktion.

Das verschwindende OR

Nun der Teil, der naive Werkzeuge stolpern lässt. Hier ist eine Methode, die eine Datei mit zwei Flags öffnet:

public Stream Open(string path) =>
    _fs.Open(path, Permissions.Read | Permissions.Write);

Frage Glass nach dem IL:

glass il Acme.Storage.dll --method Acme.Storage.FileService.Open

Die relevanten Instruktionen sind:

ldarg.0
ldfld     class IFileSystem Acme.Storage.FileService::_fs
ldarg.1
ldc.i4.3
callvirt  instance class Stream IFileSystem::Open(string, valuetype Permissions)

Es gibt keine or-Instruktion. Permissions.Read und Permissions.Write sind beide Konstanten zur Übersetzungszeit (1 und 2), also wertete der C#-Compiler 1 | 2 während der Übersetzung aus und gab ein einziges ldc.i4.3 aus. Das bitweise OR, das du geschrieben hast, wurde vom Compiler ausgeführt, nicht vom Programm. Das ist das zentrale Problem: Der Quelloperator und die Member-Namen sind überhaupt nicht im IL. Der einzige Hinweis, dass 3 etwas anderes ist als ein gewöhnlicher Integer, ist die Signatur von Open, deren letzter Parameter als valuetype Permissions typisiert ist.

Dieser Typkontext ist der Angelhaken. Glass sieht eine konstante Zahl, die in einen Parameter (oder eine lokale Variable oder ein Feld) vom Enum-Typ fließt, und wechselt von „die Zahl ausgeben” zu „die Zahl gegen die Member dieses Enums zerlegen”.

ldc.i4.3 ein nackter Integer auf dem Stack Metadaten des Enums Permissions [Flags] underlying = int None = 0 Read = 1 (Bit 0) Write = 2 (Bit 1) Execute = 4 (Bit 2) Read | Write 3 = 0b011 Bits 0 und 1 gesetzt Zerlegung: 3 → gesetzte Bits {0,1} → {Read, Write} → Rest 0 FlagsAttribute ist die Erlaubnis dazu; der Parametertyp sagt, dass die 3 ein Permissions ist keine Rest-Bits bleiben übrig, also wird am Ende kein | (Permissions)x-Cast ausgegeben

Die Zerlegungsregel und wo sie heikel wird

Glass zerlegt einen konstanten Wert, indem es die definierten Member des Enums durchläuft und Bits abgleicht. Die Regel muss drei Fälle behandeln, die ein einzeiliger „Member mit OR verbinden”-Ansatz falsch macht.

Null. Der Wert 0 wird zu None rekonstruiert, wenn ein Member mit Wert 0 existiert, andernfalls zu (Permissions)0. Er wird nie zu einem leeren OR rekonstruiert.

Kombinierte Member. Der Wert 7 könnte als Read | Write | Execute angezeigt werden, aber das Enum definiert auch All = 7. Ein guter Dekompiler bevorzugt den exakt passenden einzelnen Member: Glass gibt Permissions.All aus, weil das fast sicher das ist, worauf der Autor verwies, und es eine legale, besser lesbare Schreibweise desselben Werts ist. Die Zerlegung greift nur dann auf das OR-Verbinden einzelner Bits zurück, wenn kein benannter Member dem vollständigen Wert entspricht.

Rest-Bits. Der Wert 9 setzt Bit 0 (Read) und Bit 3, das kein Member definiert. Glass gibt Permissions.Read | (Permissions)8 aus — es benennt jedes Bit, das es kann, und castet den Rest, sodass die Rekonstruktion weiterhin exakt 9 ergibt. Es lässt die 8 nie stillschweigend fallen und erfindet nie einen Member dafür.

Deshalb ist [Flags] für die Ausgabe so wichtig. Ohne das Attribut zerlegt Glass überhaupt nicht — es behandelt das Enum als bloße Menge diskreter Werte und gibt (Permissions)3 aus, weil ein Nicht-Flags-Enum keine Zusicherung bietet, dass 3 eine sinnvolle Kombination ist und nicht ein undefinierter Wert. Das Attribut ist das eine Metadaten-Bit, das aus einem flachen Cast lesbaren Flag-Quellcode macht.

Laufzeit-Kombinationen, Tests und Löschungen

Wenn die Operanden nicht beide konstant sind, überleben die Operatoren bis ins IL, und Glass liest sie direkt zurück.

Ein Laufzeit-OR — current | extra, wo beide lokale Variablen sind — kompiliert zu einer echten or-Instruktion, und Glass rekonstruiert current | extra.

Ein von Hand geschriebener Flag-Test:

if ((perm & Permissions.Write) != 0) { ... }

kompiliert zu ldarg, ldc.i4.2, and und dann einem Sprung auf das Ergebnis. Glass sieht das and gegen eine Einzel-Member-Konstante gefolgt von einem Vergleich mit Nicht-Null und rekonstruiert den exakten bitweisen Test — es schreibt ihn nicht in HasFlag um.

Ein mit dem Helfer geschriebener Flag-Test:

if (perm.HasFlag(Permissions.Write)) { ... }

kompiliert zu einem box Permissions des Arguments und einem callvirt an System.Enum::HasFlag. Diese Aufrufstelle ist unverkennbar, also gibt Glass perm.HasFlag(Permissions.Write) wortgetreu wieder. Die beiden Formen getrennt zu halten ist wichtig: Sie verhalten sich für ein einzelnes Flag identisch, unterscheiden sich aber bei Argumenten mit kombinierten Flags, und wenn du unbekannten Code prüfst, willst du lesen, was der Autor tatsächlich geschrieben hat, und nicht eine normalisierte Umschreibung.

Ein Flag löschen — perm & ~Permissions.Write — kompiliert die Konstante ~2 zu 0xFFFFFFFD, zur Übersetzungszeit gefaltet, und dann ein and. Hier erkennt Glass die Alle-Bits-außer-einem-Maske gegen die Enum-Breite und rekonstruiert perm & ~Permissions.Write statt perm & (Permissions)4294967293, was korrekt, aber unlesbar ist.

Es von Anfang bis Ende sehen

Dekompiliere die gesamte Methode, und du bekommst die lesbare Form zurück:

glass decompile Acme.Storage.dll --method Acme.Storage.FileService.Configure
public void Configure(Permissions baseline)
{
    Permissions effective = baseline | Permissions.Read;   // Laufzeit-or
    if (effective.HasFlag(Permissions.Execute))            // Aufruf an Enum.HasFlag
        effective &= ~Permissions.Write;                   // and mit gefalteter ~Maske
    _current = effective;
}

Jeder Operator hier wurde aus einem anderen IL-Signal wiederhergestellt: das or aus einem echten or-Opcode, das HasFlag aus dem geboxten virtuellen Aufruf, die Löschung aus einem and gegen ein gefaltetes Komplement. Nichts davon war als Quellcode gespeichert. Das ist die gesamte Aufgabe eines Dekompilers — die Signale zu lesen, die der Compiler hinterlassen hat, und die Absicht darüber wieder aufzubauen.

Warum sich das Verständnis lohnt

Flag-Enums tauchen überall in echten Assemblys auf: Dateizugriffsmodi, binding flags, Regex-Optionen, Serialisierungseinstellungen, deine eigenen Feature-Schalter. Wenn du legitime Arbeit leistest — eine Bibliothek debuggen, deren Quellcode du nicht hast, ein NuGet-Paket prüfen, bevor du ihm vertraust, lernen, wie ein Framework seine Optionen verdrahtet, oder Interop-Grenzen kontrollieren — kostet dich ein Dekompiler, der Read | Write stillschweigend zu 3 abflacht, genau das Eine, weswegen du gekommen bist: die Absicht des Autors. Glass.NET ist kostenlos, du kannst also jedes flag-lastige Assembly auf diese Weise ohne Lizenzschranke lesen, und weil die Rekonstruktion auf den echten Metadaten und dem echten IL beruht, kannst du darauf vertrauen, dass Read | Write wirklich 3 ist und dass ein | (Permissions)8 am Ende bedeutet, dass es tatsächlich ein Bit gab, das niemand benannt hat.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.