Skip to content
← Tous les articles
· Delta1 Labs FileLocalTypesGlass

Décompiler les types locaux de fichier : retrouver le modificateur file à partir d'un nom de métadonnées mutilé

Le modificateur file de C# 11 n'a ni attribut ni indicateur. Le compilateur l'encode en mutilant le nom du type en une forme imprononçable <File>F0__Type et en émettant le type comme interne à l'assembly. Un décompilateur qui imprime ce nom tel quel produit du code qui ne compile pas. Voici comment Glass.NET relit cette mutilation vers file class.

C# 11 a ajouté le modificateur d’accès file : un type déclaré file class Parser n’est visible qu’à l’intérieur de l’unique fichier source qui le déclare. Deux fichiers d’un même projet — ou deux fichiers générés par du code qui émettent chacun un utilitaire — peuvent tous deux déclarer un file class Parser sans entrer en collision, et aucun ne peut être atteint depuis ailleurs. Il existe surtout au bénéfice des générateurs de code, qui émettent des types de support dans la compilation de l’utilisateur et ne doivent pas laisser fuiter des noms qui heurteraient le code propre de l’utilisateur ou celui d’un autre générateur.

Du point de vue d’un décompilateur, file est un problème intéressant car, comme plusieurs fonctionnalités récentes de C#, il ne laisse aucun attribut derrière lui. Pas de marqueur [FileLocal], pas de modificateur personnalisé, pas d’indicateur dans la ligne TypeDef disant « ce type a été déclaré file ». Le compilateur encode le modificateur dans le seul endroit qui survit jusqu’aux métadonnées et qui est garanti unique : le nom du type. Il mutile le nom déclaré en une forme qui n’est délibérément pas un identifiant C# légal, et abaisse la visibilité à interne à l’assembly. Un décompilateur qui lit la table TypeDef et imprime ce qu’il trouve émettra un type nommé <Validators>F0__Parser avec une accessibilité internal — ce qui n’est ni ce que l’auteur a écrit, ni quelque chose que le compilateur C# acceptera. Reconstruire l’original signifie reconnaître la mutilation, l’inverser, et replacer le type retrouvé dans un conteneur de fichier.

Cet article parcourt l’aller-retour complet : le code source, les métadonnées qu’émet le compilateur, et le C# que Glass.NET reconstruit.

source — Validators.csfile sealed class PhoneRule { ... }visible only in this filemetadata — TypeDef row.class private sealed'<Validators>F0__PhoneRule'no attribute · name is the signalGlass reconstructionstrip <File>F0__restore file modifierre-bucket by file idthe mangled name, decoded'<Validators>F0__PhoneRule'<Validators> = file identifier (source file name)F0 = disambiguating ordinal · __ = separator · PhoneRule = declared namewhy it cannot be printed verbatimangle brackets are reserved for compiler-generated names — '<Validators>F0__PhoneRule' is not a legal C# identifier, so emitting it as-is yields code that will not compile

Ce que l’auteur a écrit

Un type file garde généralement un petit utilitaire privé à un seul fichier. Ici Validators.cs contient une règle locale de fichier plus une façade publique qui l’utilise :

// 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 est de portée file, donc rien hors de Validators.cs ne peut le nommer — ni un autre fichier du même assembly, ni un consommateur référençant la DLL. Contact, déclaré normalement, l’appelle librement car il vit dans le même fichier.

Ce qu’émet le compilateur

Deux choses arrivent à PhoneRule sur le chemin des métadonnées. D’abord, son nom est mutilé. Le compilateur construit un nom de métadonnées à partir d’un identifiant de fichier (dérivé du nom du fichier source), d’un ordinal qui désambiguïse les fichiers partageant par hasard un nom, et du nom du type déclaré, enveloppé de sorte que le résultat ne puisse pas s’écrire en code source :

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

Remarquez ce qui n’y est pas : aucun CompilerGeneratedAttribute, aucun attribut personnalisé d’aucune sorte annonçant le modificateur. Le nom <Validators>F0__PhoneRule est la trace entière du mot-clé file. Les < et > sont l’indice — ils sont réservés précisément pour que le code utilisateur ne puisse jamais produire un identifiant entrant en collision avec un nom synthétisé par le compilateur.

Ensuite, l’accessibilité du type tombe à interne à l’assembly. Un type de premier niveau ne peut être que public ou interne dans le système de types de la CLI — il n’y a pas de visibilité plus fine au niveau des métadonnées — donc le compilateur émet PhoneRule comme interne et s’appuie sur le nom imprononçable pour imposer la règle plus forte « un seul fichier » au moment de la compilation. Dans ILDasm, un type interne de premier niveau se lit .class private, c’est pourquoi la ligne ci-dessus dit private même si vous n’avez jamais écrit private.

Le site d’appel dans Contact.Check référence le nom mutilé directement, car au moment où l’IL est émis le seul nom que possède PhoneRule est le nom mutilé :

.method public hidebysig static bool Check (string phone) cil managed
{
  ldarg.0
  call bool '<Validators>F0__PhoneRule'::IsValid(string)
  ret
}

Chaque référence à un type local de fichier dans son propre fichier — types de base, types de champ, appels de méthode, typeof — porte ce jeton mutilé. Et comme les règles de C# interdisent à un type local de fichier d’apparaître dans la signature d’un membre non-file, le jeton mutilé est garanti de n’apparaître qu’à l’intérieur des types issus du même fichier source. Cette limite compte pour la reconstruction : Glass n’a jamais à poursuivre une référence mutilée au travers d’une frontière de fichier.

La décompilation naïve, et pourquoi elle échoue

Un décompilateur qui parcourt la table TypeDef et rend chaque ligne littéralement produit ceci :

internal sealed class <Validators>F0__PhoneRule   // ne compile pas
{
    public static bool IsValid(string s) => s.Length == 10;
}

C’est faux pour deux raisons. Le nom <Validators>F0__PhoneRule n’est pas un identifiant légal, donc le fichier ne s’analyse même pas. Et internal est l’accessibilité des métadonnées, pas le modificateur source que l’auteur a choisi — réémettre internal écarte entièrement l’intention file et, pire, rendrait le type visible à tout l’assembly si vous pouviez d’une façon ou d’une autre le renommer, changeant le sens du programme. Un décompilateur fidèle doit défaire les deux transformations ensemble.

Comment Glass.NET le relit

Glass traite le nom mutilé comme un jeton structuré plutôt que comme une chaîne opaque. Sa reconstruction d’un nom de type exécute un petit décodeur :

  • Un type de premier niveau dont le nom correspond à < file >F ordinal __ name est un type local de fichier. Glass le découpe en identifiant de fichier (Validators), ordinal (0) et nom déclaré (PhoneRule).
  • Le nom déclaré est restauré comme identifiant source du type, de sorte que PhoneRule apparaît exactement comme il a été écrit.
  • Le modificateur est reconstruit comme file, et non le internal que rapportent les métadonnées — la forme mutilée est une preuve plus forte de l’intention de l’auteur que l’accessibilité au niveau de l’assembly, que le compilateur a été contraint d’utiliser uniquement parce qu’il n’en existe pas de plus étroite dans la couche des métadonnées.
  • Chaque référence mutilée à ce type ailleurs dans l’assembly — le call '<Validators>F0__PhoneRule'::IsValid dans Contact, tout usage de champ ou de type de base — est réécrite vers le PhoneRule restauré.

L’identifiant de fichier est plus qu’une décoration. Lorsque Glass réagence le code source, il utilise le conteneur <Validators> pour regrouper chaque type qui le partage, de sorte que les types locaux de fichier atterrissent ensemble dans un fichier retrouvé qui reflète leur regroupement d’origine — et, de façon cruciale, deux fichiers source distincts qui ont chacun déclaré un file class Parser (noms mutilés distincts, <Validators>F0__Parser et <Rules>F0__Parser) sont reconstruits dans des fichiers séparés, qui est le seul agencement qui compile. Les rassembler dans un seul fichier réintroduirait le conflit de noms que le modificateur file a été inventé pour empêcher.

Le code source reconstruit fait l’aller-retour proprement :

// Glass regroupe ce type dans un fichier retrouvé, identifié par son 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);
    }
}

Le modificateur file est de retour, le nom mutilé a disparu de la déclaration comme du site d’appel, et la façade publique Contact se lit exactement comme dans le code source.

Le faire depuis la CLI

Pointez Glass vers l’assembly et laissez-le tout reconstruire, regroupement de fichiers compris :

# Reconstruire vers une arborescence source ; les types locaux de fichier sont regroupés par file identifier
glass decompile Validators.dll -o ./recovered

# Inspecter un seul type, nom mutilé compris
glass decompile Validators.dll --type PhoneRule

Quand vous voulez voir les métadonnées brutes sur lesquelles le décodeur travaille — la ligne .class private avec le nom <Validators>F0__PhoneRule et le jeton call mutilé — descendez à la vue IL :

glass il Validators.dll --type "<Validators>F0__PhoneRule"

Voir le nom mutilé à côté du file class retrouvé est la façon la plus claire de comprendre ce que le modificateur coûte réellement à la couche des métadonnées : rien d’autre qu’une convention de nommage et une accessibilité abaissée, reconstruites en relisant la convention.

Pourquoi cela compte

Les types locaux de fichier apparaissent le plus souvent dans du code généré, qui est précisément le code que vous avez le plus de chances de lire dans un décompilateur — vous avez une DLL, un générateur de code s’est exécuté lors de sa compilation, et vous voulez comprendre ce qu’il a produit. Si votre outil imprime internal class <Thing>F0__Helper, vous n’apprenez presque rien et vous ne pouvez pas recompiler ce que vous voyez. Retrouver le vrai file class Helper, regroupé comme le générateur l’a émis, transforme un vidage mutilé illisible en le code qui a réellement été écrit — ce qui est tout l’intérêt de décompiler votre propre assembly ou celui d’un tiers pour le comprendre, le déboguer ou interopérer avec lui.

Glass.NET est un décompilateur .NET gratuit doté d’une interface Avalonia native à Visual Studio et d’une interface CLI/MCP, conçu pour reconstruire précisément ces constructions modernes de C# — celles qui vivent dans des conventions de métadonnées plutôt que dans des attributs — afin que ce que vous relisez soit le code source, et non la comptabilité du compilateur.

Essayez Nebula.NET

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