Dateilokale Typen dekompilieren: den file-Modifizierer aus einem verstümmelten Metadatennamen zurückgewinnen
Der file-Modifizierer von C# 11 hat weder Attribut noch Flag. Der Compiler kodiert ihn, indem er den Typnamen in eine unaussprechliche Form <File>F0__Type verstümmelt und den Typ als assembly-intern ausgibt. Ein Dekompilierer, der diesen Namen wörtlich ausgibt, erzeugt Code, der nicht kompiliert. So liest Glass.NET die Verstümmelung zurück zu file class.
C# 11 führte den Zugriffsmodifizierer file ein: Ein als file class Parser deklarierter Typ ist nur innerhalb der einzelnen Quelldatei sichtbar, die ihn deklariert. Zwei Dateien desselben Projekts — oder zwei quellcodegenerierte Dateien, die jeweils einen Helfer ausgeben — können beide einen file class Parser deklarieren, ohne zu kollidieren, und keiner lässt sich von irgendwo sonst aus berühren. Er existiert vor allem zum Nutzen von Quellcodegeneratoren, die Hilfstypen in die Kompilierung des Nutzers einbringen und keine Namen entweichen lassen dürfen, die mit dem eigenen Code des Nutzers oder dem eines anderen Generators kollidieren.
Aus Sicht eines Dekompilierers ist file ein interessantes Problem, denn wie mehrere jüngere C#-Funktionen hinterlässt er kein Attribut. Keine [FileLocal]-Markierung, kein benutzerdefinierter Modifizierer, kein Flag in der TypeDef-Zeile, das sagt „dieser Typ wurde file deklariert”. Der Compiler kodiert den Modifizierer an der einzigen Stelle, die bis in die Metadaten überlebt und garantiert eindeutig ist: im Namen des Typs. Er verstümmelt den deklarierten Namen in eine Form, die absichtlich kein gültiger C#-Bezeichner ist, und senkt die Sichtbarkeit auf assembly-intern. Ein Dekompilierer, der die TypeDef-Tabelle liest und ausgibt, was er findet, gibt einen Typ namens <Validators>F0__Parser mit internal-Zugänglichkeit aus — was weder das ist, was der Autor schrieb, noch etwas, das der C#-Compiler akzeptiert. Das Original zu rekonstruieren bedeutet, die Verstümmelung zu erkennen, sie umzukehren und den zurückgewonnenen Typ wieder in einen Datei-Behälter einzuordnen.
Dieser Beitrag durchläuft den vollständigen Hin- und Rückweg: den Quellcode, die Metadaten, die der Compiler ausgibt, und das C#, das Glass.NET rekonstruiert.
Was der Autor geschrieben hat
Ein file-Typ hält in der Regel einen kleinen Helfer privat zu einer Datei. Hier enthält Validators.cs eine dateilokale Regel plus eine öffentliche Fassade, die sie verwendet:
// Validators.cs
file sealed class PhoneRule
{
public static bool IsValid(string s) => s.Length == 10;
}
public static class Contact
{
public static bool Check(string phone) => PhoneRule.IsValid(phone);
}
PhoneRule hat file-Gültigkeitsbereich, also kann nichts außerhalb von Validators.cs ihn benennen — keine andere Datei derselben Assembly, kein Konsument, der die DLL referenziert. Contact, normal deklariert, ruft ihn ungehindert auf, weil er in derselben Datei lebt.
Was der Compiler ausgibt
Zwei Dinge geschehen PhoneRule auf dem Weg in die Metadaten. Zuerst wird sein Name verstümmelt. Der Compiler baut einen Metadatennamen aus einer Datei-Kennung (abgeleitet vom Namen der Quelldatei), einer Ordinalzahl, die Dateien unterscheidet, die zufällig einen Namen teilen, und dem deklarierten Typnamen, so verpackt, dass sich das Ergebnis nicht in Quellcode schreiben lässt:
.class private sealed auto ansi beforefieldinit
'<Validators>F0__PhoneRule'
extends [System.Runtime]System.Object
{
.method public hidebysig static
bool IsValid (string s) cil managed
{
ldarg.0
callvirt instance int32 [System.Runtime]System.String::get_Length()
ldc.i4.s 10
ceq
ret
}
}
Beachten Sie, was nicht da ist: kein CompilerGeneratedAttribute, kein benutzerdefiniertes Attribut irgendeiner Art, das den Modifizierer ankündigt. Der Name <Validators>F0__PhoneRule ist die gesamte Aufzeichnung des Schlüsselworts file. Die < und > sind der Hinweis — sie sind genau deshalb reserviert, damit Nutzercode niemals einen Bezeichner erzeugen kann, der mit einem compilergenerierten kollidiert.
Zweitens sinkt die Zugänglichkeit des Typs auf assembly-intern. Ein Typ oberster Ebene kann im CLI-Typsystem nur public oder intern sein — es gibt keine feinere Sichtbarkeit auf Metadatenebene — also gibt der Compiler PhoneRule als intern aus und verlässt sich auf den unaussprechlichen Namen, um die stärkere „nur eine Datei”-Regel zur Kompilierzeit durchzusetzen. In ILDasm liest sich ein interner Typ oberster Ebene als .class private, weshalb die Zeile oben private sagt, obwohl Sie nie private geschrieben haben.
Die Aufrufstelle in Contact.Check referenziert den verstümmelten Namen direkt, denn zu dem Zeitpunkt, an dem IL ausgegeben wird, ist der einzige Name, den PhoneRule hat, der verstümmelte:
.method public hidebysig static bool Check (string phone) cil managed
{
ldarg.0
call bool '<Validators>F0__PhoneRule'::IsValid(string)
ret
}
Jede Referenz auf einen dateilokalen Typ innerhalb seiner eigenen Datei — Basistypen, Feldtypen, Methodenaufrufe, typeof — trägt dieses verstümmelte Token. Und da die C#-Regeln einem dateilokalen Typ verbieten, in der Signatur eines Nicht-file-Mitglieds aufzutauchen, ist garantiert, dass das verstümmelte Token nur innerhalb der Typen erscheint, die aus derselben Quelldatei stammen. Diese Grenze ist für die Rekonstruktion wichtig: Glass muss einer verstümmelten Referenz nie über eine Dateigrenze hinweg nachjagen.
Die naive Dekompilierung, und warum sie scheitert
Ein Dekompilierer, der die TypeDef-Tabelle durchläuft und jede Zeile wörtlich wiedergibt, erzeugt dies:
internal sealed class <Validators>F0__PhoneRule // kompiliert nicht
{
public static bool IsValid(string s) => s.Length == 10;
}
Das ist aus zwei Gründen falsch. Der Name <Validators>F0__PhoneRule ist kein gültiger Bezeichner, also wird die Datei nicht einmal geparst. Und internal ist die Metadaten-Zugänglichkeit, nicht der Quellcode-Modifizierer, den der Autor wählte — ein erneutes Ausgeben von internal verwirft die file-Absicht vollständig und würde, schlimmer noch, den Typ für die ganze Assembly sichtbar machen, falls Sie ihn irgendwie umbenennen könnten, und so die Bedeutung des Programms ändern. Ein treuer Dekompilierer muss beide Transformationen zusammen rückgängig machen.
Wie Glass.NET es zurückliest
Glass behandelt den verstümmelten Namen als strukturiertes Token statt als undurchsichtige Zeichenkette. Seine Rekonstruktion eines Typnamens führt einen kleinen Dekodierer aus:
- Ein Typ oberster Ebene, dessen Name dem Muster
<file>Fordinal__name entspricht, ist ein dateilokaler Typ. Glass zerlegt ihn in die Datei-Kennung (Validators), die Ordinalzahl (0) und den deklarierten Namen (PhoneRule). - Der deklarierte Name wird als Quellcode-Bezeichner des Typs wiederhergestellt, sodass
PhoneRulegenau so erscheint, wie es geschrieben wurde. - Der Modifizierer wird als
filerekonstruiert, nicht als dasinternal, das die Metadaten melden — die verstümmelte Form ist ein stärkerer Beleg für die Absicht des Autors als die Zugänglichkeit auf Assembly-Ebene, die der Compiler nur deshalb verwenden musste, weil es auf der Metadatenschicht keine engere gibt. - Jede verstümmelte Referenz auf diesen Typ an anderer Stelle in der Assembly — der
call '<Validators>F0__PhoneRule'::IsValidinContact, jede Feld- oder Basistyp-Verwendung — wird auf das wiederhergestelltePhoneRuleumgeschrieben.
Die Datei-Kennung ist mehr als Zierde. Wenn Glass den Quellcode wieder auslegt, nutzt es den <Validators>-Behälter, um jeden Typ zu gruppieren, der ihn teilt, sodass dateilokale Typen zusammen in einer zurückgewonnenen Datei landen, die ihre ursprüngliche Gruppierung widerspiegelt — und, entscheidend, zwei verschiedene Quelldateien, die jeweils einen file class Parser deklariert haben (verschiedene verstümmelte Namen, <Validators>F0__Parser und <Rules>F0__Parser), werden in getrennte Dateien rekonstruiert, was die einzige Anordnung ist, die kompiliert. Sie in eine einzige Datei zusammenzufassen würde den Namenskonflikt wieder einführen, den der file-Modifizierer verhindern sollte.
Der rekonstruierte Quellcode macht den Hin- und Rückweg sauber:
// Glass gruppiert diesen Typ in eine zurückgewonnene Datei, identifiziert über seinen file identifier
file sealed class PhoneRule
{
public static bool IsValid(string s)
{
return s.Length == 10;
}
}
public static class Contact
{
public static bool Check(string phone)
{
return PhoneRule.IsValid(phone);
}
}
Der file-Modifizierer ist zurück, der verstümmelte Name ist sowohl aus der Deklaration als auch aus der Aufrufstelle verschwunden, und die öffentliche Contact-Fassade liest sich genau wie im Quellcode.
Über die CLI
Richten Sie Glass auf die Assembly und lassen Sie es das Ganze rekonstruieren, Dateigruppierung inklusive:
# In einen Quellcode-Baum rekonstruieren; dateilokale Typen werden nach file identifier neu gruppiert
glass decompile Validators.dll -o ./recovered
# Einen einzelnen Typ untersuchen, verstümmelter Name und alles
glass decompile Validators.dll --type PhoneRule
Wenn Sie die Rohmetadaten sehen möchten, mit denen der Dekodierer arbeitet — die .class private-Zeile mit dem Namen <Validators>F0__PhoneRule und das verstümmelte call-Token — wechseln Sie in die IL-Ansicht:
glass il Validators.dll --type "<Validators>F0__PhoneRule"
Den verstümmelten Namen neben dem zurückgewonnenen file class zu sehen ist der klarste Weg zu verstehen, was der Modifizierer auf der Metadatenschicht tatsächlich kostet: nichts als eine Namenskonvention und eine herabgesetzte Zugänglichkeit, rekonstruiert, indem man die Konvention zurückliest.
Warum das wichtig ist
Dateilokale Typen tauchen am häufigsten in generiertem Code auf, was genau der Code ist, den Sie am ehesten in einem Dekompilierer lesen — Sie haben eine DLL, ein Quellcodegenerator lief während ihrer Kompilierung, und Sie wollen verstehen, was er erzeugt hat. Wenn Ihr Werkzeug internal class <Thing>F0__Helper ausgibt, lernen Sie fast nichts und können das Gesehene nicht neu kompilieren. Den echten file class Helper zurückzugewinnen, gruppiert so, wie der Generator ihn ausgab, verwandelt einen unlesbaren verstümmelten Abzug zurück in den Code, der tatsächlich geschrieben wurde — was der ganze Sinn ist, Ihre eigene oder die Assembly eines Dritten zu dekompilieren, um sie zu verstehen, zu debuggen oder mit ihr zu interagieren.
Glass.NET ist ein kostenloser .NET-Dekompilierer mit einer Visual-Studio-nativen Avalonia-Oberfläche und einer CLI/MCP-Schnittstelle, gebaut, um genau diese modernen C#-Konstrukte zu rekonstruieren — die, die in Metadatenkonventionen statt in Attributen leben — damit das, was Sie zurücklesen, der Quellcode ist und nicht die Buchführung des Compilers.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.