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

Wie Glass Typweiterleitung und die ExportedType-Tabelle verfolgt

Wenn ein Typ von mscorlib nach System.Runtime wandert, behält das alte Assembly eine Weiterleitung: eine ExportedType-Zeile mit dem Weiterleitungs-Flag, die auf ein neues AssemblyRef zeigt. Eine Weiterleitung hat keine Member, deshalb darf ein Decompiler sie nicht als Typ darstellen: Er muss der Weiterleitung bis zur echten Definition folgen. Dieser Artikel zeigt, wo die Weiterleitung in den Metadaten liegt, wie Glass.NET eine Weiterleitungskette auflöst, wie es sich sauber verhält, wenn das Ziel-Assembly fehlt, und wie es [assembly: TypeForwardedTo(...)] wieder ausgibt, wenn man die Fassade selbst dekompiliert.

Wenige .NET-Metadatenelemente stiften so viel Verwirrung wie die Typweiterleitung. Man öffnet ein altes Assembly – sagen wir mscorlib oder eine netstandard-Fassade –, fragt die Werkzeuge nach System.ValueTuple, und der Typ wird aufgeführt, hat aber keinen Code. Es ist kein Platzhalter, den der Compiler zu füllen vergaß. Es ist eine Weiterleitung: ein einzeiliges Schild, das sagt, der Typ lebt jetzt physisch woanders. Ein Decompiler, der das nicht versteht, zeigt eine leere Klasse oder eine unaufgelöste Referenz, und in beiden Fällen hat er an seiner einen Aufgabe versagt. Es geht hier darum, wie Glass.NET diesen Fall richtig behandelt: wo die Weiterleitung in den Metadaten liegt, wie Glass ihr folgt (auch über eine Kette hinweg), was es tut, wenn das Ziel nirgends zu finden ist, und wie es die Weiterleitung wieder ausgibt, wenn man das Weiterleitungs-Assembly selbst dekompiliert.

Glass.NET ist ein kostenloser .NET-Decompiler und Assembly-Inspektor mit einer Visual-Studio-nativen Avalonia-Oberfläche und einem glass-Kommandozeilenwerkzeug. Alles hier Beschriebene ist statische Analyse: Glass liest die Metadatentabellen und Methodenrümpfe von Assemblys, die man bereits besitzt und zu inspizieren berechtigt ist. Es lädt oder führt das Ziel niemals aus.

Warum Weiterleitungen existieren

Im Laufe des .NET-Lebens wurde die Basisklassenbibliothek mehr als einmal umgestellt. Typen, die unter dem .NET Framework in mscorlib oder System.Core lebten, wurden beim Übergang zu .NET Core und System.Runtime in einen feingranularen Satz von Implementierungs-Assemblys aufgeteilt – System.Runtime, System.Collections, System.Linq und so weiter. System.ValueTuple ist ein gutes konkretes Beispiel: Es wurde zuerst in einem eigenständigen NuGet-Paket System.ValueTuple.dll ausgeliefert und später in die Plattform gezogen, sodass der Typ System.ValueTuple nun in System.Runtime lebt.

Das Problem ist der ganze Code, der bereits gegen den alten Ort kompiliert wurde. Eine Methode, die gegen System.ValueTuple.dll kompiliert wurde, hat einen TypeRef in jenes Assembly in ihren Metadaten eingebrannt. Verschwände der Typ dort einfach, zerbräche jede solche Referenz beim Laden. Die Lösung ist, eine Weiterleitung zu hinterlassen: Das alte Assembly behält einen Eintrag für den Typ, hält aber statt einer Definition einen Zeiger auf das Assembly, in das der Typ gewandert ist. Alte Referenzen lösen sich weiterhin gegen das alte Assembly auf; die Laufzeit folgt der Weiterleitung still und bindet an den echten Typ. Das Fassaden-Assembly wird zu einer dünnen Schicht aus nichts als Weiterleitungen.

Der Quellcode, der eine Weiterleitung erzeugt, ist ein einziges Attribut auf Assembly-Ebene:

[assembly: TypeForwardedTo(typeof(System.ValueTuple))]

Man setzt es in das alte Assembly (das, welches den Typ nicht mehr enthält), mit Verweis auf das neue Assembly, das ihn enthält. Der Compiler macht daraus Metadaten.

Wo die Weiterleitung liegt: die ExportedType-Tabelle

Das Attribut kompiliert zu einer Zeile in der Metadatentabelle ExportedType (0x27). Eine ExportedType-Zeile hat vier Spalten, die hier wichtig sind:

  • Flags — ein TypeAttributes-Wert. Eine Weiterleitung setzt das Bit IsTypeForwarder, 0x00200000. Dieses Bit unterscheidet eine Weiterleitung vom anderen Gebrauch von ExportedType (dem Re-Export eines Typs aus einem Sekundärmodul eines mehrmoduligen Assemblys).
  • TypeName / TypeNamespace — der Name des weitergeleiteten Typs, z. B. ValueTuple im Namespace System.
  • Implementation — ein codierter Index. Bei einer Weiterleitung zeigt er auf eine AssemblyRef-Zeile: das Assembly, in dem der Typ nun lebt.

Entscheidend ist: Es gibt keine Zeile in der TypeDef-Tabelle für einen weitergeleiteten Typ. Die ExportedType-Zeile trägt nur Identität – Name, Namespace, Ziel – und sonst nichts. Keine Felder, keine Methoden, kein Basistyp, kein Layout. Genau das ist der Sinn: Eine Weiterleitung ist eine Adresse, kein Haus.

In einer Ansicht im ILDasm-Stil sehen die Weiterleitung und das AssemblyRef, von dem sie abhängt, so aus:

.assembly extern System.Runtime
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A)
  .ver 8:0:0:0
}

.class extern forwarder System.ValueTuple
{
  .assembly extern System.Runtime
}

Das Schlüsselwort forwarder an .class extern ist genau das IsTypeForwarder-Flag; das verschachtelte .assembly extern System.Runtime ist die Implementation-Spalte, aufgelöst zum AssemblyRef. Nirgendwo in diesem Assembly gibt es einen .class-Rumpf – nur dieses externe Schild.

Was ein naiver Decompiler falsch macht

Gibt man dieses Assembly einem Werkzeug, das die ExportedType-Tabelle durchläuft, ohne das Weiterleitungs-Bit gesondert zu behandeln, passiert eines von zwei schlechten Dingen.

Behandelt es die ExportedType-Zeile wie einen anzuzeigenden Typ, bekommt man eine leere Hülle:

// FALSCH — eine Weiterleitung dargestellt, als wäre sie eine Definition
namespace System
{
    public struct ValueTuple
    {
        // (keine Member — dieses Assembly hat keine Definition für den Typ)
    }
}

Dieser Rumpf ist eine Fiktion. ValueTuple hat acht und mehr Konstruktoren, Create-Überladungen, IComparable, ITuple – von denen keines hier ist, weil keines in diesem Assembly ist. Eine leere Struct zu zeigen impliziert, der Typ habe keine Member, was schlicht falsch ist.

Der andere Fehlermodus ist, eine Referenz auf System.ValueTuple direkt gegen die Fassade aufzulösen, keinen TypeDef zu finden und sie als unaufgelösten Typ an die Oberfläche zu bringen: eine rote Schlängellinie in der Oberfläche oder /* unresolved */ ValueTuple. Ebenfalls falsch: Der Typ löst sich bestens auf, nur einen Sprung entfernt.

Wie Glass der Weiterleitung folgt

Glass behandelt das Weiterleitungs-Bit gesondert. Wenn es eine Typreferenz auflöst oder wenn man es bittet, einen Typ nach Namen zu dekompilieren, hält es nicht bei der ExportedType-Zeile an. Es liest die Implementation, löst das AssemblyRef gegen die Menge der geladenen Assemblys auf (derselbe Auflöser, den Glass ohnehin für gewöhnliche Referenzen nutzt – Framework-Verzeichnisse, der Ordner des geöffneten Assemblys und jeder hinzugefügte Suchpfad), lädt das Ziel-Assembly und sucht dort den echten TypeDef. Es dekompiliert diesen.

Der Ablauf, einschließlich des verketteten Falls, in dem eine Weiterleitung auf ein Assembly zeigt, das den Typ seinerseits weiterleitet:

referenzierender CodeTypeRef → mscorliban alte Fassade gebundenmscorlib.dllExportedTypeforwarder → AssemblyRefkein TypeDef hiernetstandard.dllExportedTypeforwarder → AssemblyRefverkettete WeiterleitungSystem.Runtime.dllTypeDefSystem.ValueTupleechte Definition + MemberGlass folgt jedem Weiterleitungssprung bis zum echten TypeDef und dekompiliert diesenJede Fassade trägt nur ein Schild; die Definition – und all ihre Member – lebt einen oder mehrere Sprünge entfernt.

Da Weiterleitungen sich verketten können – Assembly A leitet zu B weiter, und B leitet denselben Typ weiter zu C –, folgt Glass der Implementation wiederholt, nicht nur einmal. Es geht von Weiterleitung zu Weiterleitung, bis es auf einem echten TypeDef landet, und hält dabei eine Besuchsmenge, damit ein pathologischer Zyklus von Weiterleitungen es nicht in eine Endlosschleife schickt; ein Zyklus wird als gebrochene Weiterleitung gemeldet, statt die Oberfläche einzufrieren.

Nach Namen von der Fassade aus dekompilieren

Von der Kommandozeile aus kann man glass direkt auf die alte Fassade richten und den Typ anfordern. Glass folgt der Weiterleitung für dich:

glass decompile mscorlib.dll --type System.ValueTuple
// forwarded from mscorlib.dll → System.Runtime
// definition in System.Runtime, Version=8.0.0.0
namespace System
{
    public struct ValueTuple : IEquatable<ValueTuple>,
        IStructuralEquatable, IStructuralComparable, IComparable,
        IComparable<ValueTuple>, ITuple
    {
        public static ValueTuple Create() => default;
        public override int GetHashCode() => 0;
        // ... the real members, decompiled from System.Runtime
    }
}

Das Banner // forwarded from sagt dir, dass Glass eine Assembly-Grenze überschritten hat, um hierher zu gelangen; der Rumpf ist die echte Definition aus System.Runtime, samt Member. Willst du auf der Fassade bleiben und die Weiterleitung inspizieren, statt ihr nachzujagen – etwa um genau zu prüfen, was ein netstandard-Mantel re-exportiert –, hält --no-follow-forwarders Glass an der ExportedType-Zeile an und gibt die Weiterleitung aus:

glass decompile mscorlib.dll --type System.ValueTuple --no-follow-forwarders
.class extern forwarder System.ValueTuple -> System.Runtime

Das Weiterleitungs-Assembly selbst dekompilieren

Wenn man das Fassaden-Assembly als Ganzes öffnet – nicht einen Typ, sondern das Assembly –, ist die richtige Ausgabe kein Haufen leerer Typen. Es ist die Menge der Weiterleitungsattribute, die jene ExportedType-Zeilen erzeugt haben. Glass rekonstruiert sie in der Ansicht der Assembly-Attribute:

// Assembly attributes for mscorlib.dll (facade)
[assembly: TypeForwardedTo(typeof(System.ValueTuple))]
[assembly: TypeForwardedTo(typeof(System.Collections.Generic.List<>))]
[assembly: TypeForwardedTo(typeof(System.Threading.Tasks.Task))]
// ... one per forwarder

Das ist das C#, das – neu kompiliert – dieselbe ExportedType-Tabelle wieder erzeugen würde. Eine Fassade dekompiliert sich zurück in eine Liste von TypeForwardedTo-Deklarationen – genau das, was eine Fassade ist.

Wenn das Ziel fehlt

Die Auflösung kann scheitern. Vielleicht hast du die Fassade auf der Platte, aber nicht System.Runtime, oder ein Assembly leitet in etwas weiter, das du schlicht nicht hast. Glass stürzt nicht ab und lässt den Typ nicht stillschweigend fallen. Es meldet die Weiterleitung selbst, als unaufgelöst markiert, sodass du trotzdem erfährst, wohin der Typ gewandert ist:

glass decompile oldlib.dll --type Contoso.Legacy.Widget
// unresolved forward
// Contoso.Legacy.Widget is forwarded to:
//   Contoso.Runtime, Version=3.0.0.0, PublicKeyToken=a1b2c3d4e5f60708
// target assembly not found on any search path — showing the forward only
.class extern forwarder Contoso.Legacy.Widget -> Contoso.Runtime

Der Rest des Assemblys dekompiliert normal; nur die Typen, deren Ziele fehlen, fallen auf diese reine Weiterleitungsansicht zurück. Du bekommst den vollqualifizierten Namen des Ziel-Assemblys – genug, um es zu suchen und Glass mit einem hinzugefügten Suchpfad darauf zu richten – statt einer Ausnahme oder einer Leerstelle.

Warum das zählt, wenn du echten Code liest

Typweiterleitung ist unsichtbar, bis sie es nicht mehr ist. Meistens folgt die Laufzeit den Weiterleitungen für dich, und du denkst nie daran. Doch in dem Moment, in dem du ein Fassaden-Assembly in einem Decompiler öffnest – eine BCL-Methode verfolgst, debuggst, warum eine Referenz an das Assembly bindet, an das sie bindet, einen netstandard-Mantel verstehst –, werden die Weiterleitungen plötzlich zur ganzen Geschichte, und ein Werkzeug, das sie als leere Typen oder unaufgelöste Referenzen darstellt, führt dich aktiv in die Irre. Glass behandelt die ExportedType-Tabelle als das, was sie ist: eine Menge von Schildern. Es folgt ihnen bis zu den echten Definitionen, Ketten inbegriffen; es gibt sie treu wieder aus, wenn du die Fassade dekompilierst; und es sagt dir ehrlich, wohin ein Typ gewandert ist, wenn es ihm nicht folgen kann. Das ist der Unterschied zwischen einem Decompiler, der Metadaten versteht, und einem, der sie bloß auflistet.

Nebula.NET testen

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