Skip to content
← Tous les articles
· Delta1 Labs DecompilationGlass

Comment Glass suit le réacheminement de types et la table ExportedType

Quand un type passe de mscorlib à System.Runtime, l'assembly d'origine conserve un réacheminement : une ligne ExportedType avec l'indicateur de réacheminement pointant vers un nouvel AssemblyRef. Un réacheminement n'a aucun membre, donc un décompilateur ne doit pas le représenter comme un type : il doit suivre le réacheminement jusqu'à la vraie définition. Cet article parcourt où vit le réacheminement dans les métadonnées, comment Glass.NET résout une chaîne de réacheminements, comment il se dégrade proprement quand l'assembly cible manque et comment il réémet [assembly: TypeForwardedTo(...)] quand tu décompiles la façade elle-même.

Peu d”éléments de métadonnées .NET causent autant de confusion que le réacheminement de type. Tu ouvres un ancien assembly — mscorlib, disons, ou une façade netstandard —, tu demandes à tes outils System.ValueTuple, et le type apparaît dans la liste mais n”a aucun code. Ce n”est pas une ébauche que le compilateur a oublié de remplir. C”est un réacheminement : un panneau d”une ligne qui dit que le type vit désormais physiquement ailleurs. Un décompilateur qui ne le comprend pas te montrera une classe vide, ou une référence non résolue, et dans les deux cas il a échoué à sa seule tâche. Il s”agit ici de la manière dont Glass.NET gère correctement ce cas : où vit le réacheminement dans les métadonnées, comment Glass le suit (même à travers une chaîne), ce qu”il fait quand la cible est introuvable et comment il réémet le réacheminement quand tu décompiles l”assembly de réacheminement lui-même.

Glass.NET est un décompilateur et inspecteur d”assemblys .NET gratuit, avec une interface Avalonia native de Visual Studio et un outil en ligne de commande glass. Tout ce qui est décrit ici relève de l”analyse statique : Glass lit les tables de métadonnées et les corps de méthodes d”assemblys que tu possèdes déjà et que tu es en droit d”inspecter. Il ne charge ni n”exécute jamais la cible.

Pourquoi les réacheminements existent

Au fil de la vie de .NET, la bibliothèque de classes de base a été réorganisée plus d”une fois. Les types qui vivaient dans mscorlib ou System.Core sous .NET Framework ont été répartis, lors du passage à .NET Core et System.Runtime, en un ensemble d”assemblys d”implémentation à grain fin — System.Runtime, System.Collections, System.Linq, etc. System.ValueTuple est un bon cas concret : il a d”abord été distribué dans un paquet NuGet autonome System.ValueTuple.dll, puis intégré à la plateforme, de sorte que le type System.ValueTuple vit maintenant dans System.Runtime.

Le problème, c”est tout le code déjà compilé contre l”ancien emplacement. Une méthode compilée contre System.ValueTuple.dll a un TypeRef vers cet assembly gravé dans ses métadonnées. Si le type disparaissait simplement de là, toutes ces références se briseraient au moment du chargement. La solution est de laisser derrière soi un réacheminement : l”ancien assembly conserve une entrée pour le type, mais au lieu d”une définition il contient un pointeur vers l”assembly où le type a été déplacé. Les anciennes références se résolvent toujours contre l”ancien assembly ; le runtime suit discrètement le réacheminement et se lie au vrai type. L”assembly façade devient une mince couche composée uniquement de réacheminements.

Le code source qui produit un réacheminement est un unique attribut de niveau assembly :

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

Tu le mets dans l”ancien assembly (celui qui ne contient plus le type), en référençant le nouvel assembly qui, lui, le contient. Le compilateur le transforme en métadonnées.

Où vit le réacheminement : la table ExportedType

L”attribut se compile en une ligne dans la table de métadonnées ExportedType (0x27). Une ligne ExportedType a quatre colonnes qui comptent ici :

  • Flags — une valeur TypeAttributes. Un réacheminement active le bit IsTypeForwarder, 0x00200000. Ce bit est ce qui distingue un réacheminement de l”autre usage d”ExportedType (réexporter un type depuis un module secondaire d”un assembly multi-module).
  • TypeName / TypeNamespace — le nom du type réacheminé, p. ex. ValueTuple dans l”espace de noms System.
  • Implementation — un index codé. Pour un réacheminement, il pointe vers une ligne AssemblyRef : l”assembly où le type vit désormais.

Point crucial, il n”y a aucune ligne dans la table TypeDef pour un type réacheminé. La ligne ExportedType ne porte que l”identité — nom, espace de noms, cible — et rien d”autre. Aucun champ, aucune méthode, aucun type de base, aucune disposition. C”est précisément le but : un réacheminement est une adresse, pas une maison.

Rendus dans une vue de style ILDasm, le réacheminement et l”AssemblyRef dont il dépend ressemblent à ceci :

.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
}

Le mot-clé forwarder sur .class extern est exactement l”indicateur IsTypeForwarder ; le .assembly extern System.Runtime imbriqué est la colonne Implementation résolue vers l”AssemblyRef. Il n”y a aucun corps .class nulle part dans cet assembly — seulement ce panneau externe.

Ce qu”un décompilateur naïf fait de travers

Donne cet assembly à un outil qui parcourt la table ExportedType sans traiter spécialement le bit de réacheminement et l”une des deux mauvaises choses se produit.

S”il traite la ligne ExportedType comme un type à afficher, tu obtiens une coquille vide :

// INCORRECT — un réacheminement représenté comme s''il était une définition
namespace System
{
    public struct ValueTuple
    {
        // (aucun membre — cet assembly n''a pas de définition pour le type)
    }
}

Ce corps est une fiction. ValueTuple a huit constructeurs et plus, des surcharges de Create, IComparable, ITuple — dont aucun n”est ici, parce qu”aucun n”est dans cet assembly. Montrer une struct vide implique que le type n”a aucun membre, ce qui est catégoriquement faux.

L”autre mode d”échec est de résoudre une référence à System.ValueTuple directement contre la façade, de n”y trouver aucun TypeDef et de la faire remonter comme un type non résolu : un soulignement rouge dans l”interface, ou /* unresolved */ ValueTuple. Tout aussi faux : le type se résout parfaitement, à un seul saut de distance.

Comment Glass suit le réacheminement

Glass traite spécialement le bit de réacheminement. Quand il résout une référence de type, ou quand tu lui demandes de décompiler un type par son nom, il ne s”arrête pas à la ligne ExportedType. Il lit l”Implementation, résout l”AssemblyRef contre l”ensemble des assemblys chargés (le même résolveur que Glass utilise déjà pour les références ordinaires — répertoires du framework, dossier de l”assembly que tu as ouvert et tout chemin de recherche que tu as ajouté), charge l”assembly cible et y cherche le vrai TypeDef. Il décompile celui-ci.

Le flux, y compris le cas chaîné où un réacheminement pointe vers un assembly qui à son tour réachemine le type plus loin :

code référençantTypeRef → mscorliblié à l''ancienne façademscorlib.dllExportedTypeforwarder → AssemblyRefaucun TypeDef icinetstandard.dllExportedTypeforwarder → AssemblyRefréacheminement chaînéSystem.Runtime.dllTypeDefSystem.ValueTuplevraie définition + membresGlass suit chaque saut de réacheminement jusqu''au vrai TypeDef, puis le décompileChaque façade ne porte qu''un panneau ; la définition — et tous ses membres — vit à un ou plusieurs sauts.

Comme les réacheminements peuvent se chaîner — l”assembly A réachemine vers B, et B réachemine le même type plus loin vers C —, Glass suit l”Implementation de manière répétée, pas une seule fois. Il va de réacheminement en réacheminement jusqu”à atterrir sur un vrai TypeDef, tout en gardant un ensemble de visités pour qu”un cycle pathologique de réacheminements ne l”entraîne pas dans une boucle infinie ; un cycle est signalé comme un réacheminement rompu plutôt que de figer l”interface.

Décompiler par nom depuis la façade

Depuis la ligne de commande, tu peux pointer glass directement sur l”ancienne façade et demander le type. Glass fait le suivi du réacheminement pour toi :

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
    }
}

La bannière // forwarded from t”indique que Glass a franchi une frontière d”assembly pour arriver ici ; le corps est la véritable définition issue de System.Runtime, membres compris. Si tu veux rester sur la façade et inspecter le réacheminement au lieu de le poursuivre — par exemple pour auditer exactement ce qu”un enrobage netstandard réexporte —, --no-follow-forwarders arrête Glass à la ligne ExportedType et imprime le réacheminement :

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

Décompiler l”assembly de réacheminement lui-même

Quand tu ouvres l”assembly façade dans son ensemble — pas un type, mais l”assembly —, la bonne sortie n”est pas un tas de types vides. C”est l”ensemble des attributs de réacheminement qui ont produit ces lignes ExportedType. Glass les reconstruit dans la vue des attributs de l”assembly :

// 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

C”est le C# qui, recompilé, régénérerait la même table ExportedType. Une façade se décompile en une liste de déclarations TypeForwardedTo — ce qu”une façade est exactement.

Quand la cible manque

La résolution peut échouer. Tu peux avoir la façade sur le disque mais pas System.Runtime, ou un assembly peut réacheminer vers quelque chose que tu n”as tout simplement pas. Glass ne plante pas et n”abandonne pas le type en silence. Il signale le réacheminement lui-même, marqué comme non résolu, pour que tu saches malgré tout où le type est parti :

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

Le reste de l”assembly se décompile normalement ; seuls les types dont les cibles sont absentes se rabattent sur cette vue de réacheminement seul. Tu obtiens le nom complet de l”assembly de destination — assez pour aller le chercher et pointer Glass vers lui avec un chemin de recherche ajouté — au lieu d”une exception ou d”un blanc.

Pourquoi cela compte quand tu lis du vrai code

Le réacheminement de type est invisible jusqu”à ce qu”il ne le soit plus. La plupart du temps, le runtime suit les réacheminements pour toi et tu n”y penses jamais. Mais à l”instant où tu ouvres un assembly façade dans un décompilateur — en traçant une méthode de la BCL, en déboguant pourquoi une référence se lie à l”assembly qu”elle lie, en comprenant un enrobage netstandard —, les réacheminements deviennent soudain toute l”histoire, et un outil qui les représente comme des types vides ou des références non résolues t”induit activement en erreur. Glass traite la table ExportedType pour ce qu”elle est : un ensemble de panneaux. Il les suit jusqu”aux vraies définitions, chaînes comprises ; il les réémet fidèlement quand tu décompiles la façade ; et il te dit honnêtement où un type est parti quand il ne peut pas le suivre. C”est la différence entre un décompilateur qui comprend les métadonnées et un qui se contente de les lister.

Essayez Nebula.NET

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