Skip to content
← Tous les articles
· Delta1 Labs PDBGlass

Récupérer les vrais noms de variables, fichiers source et numéros de ligne depuis les PDB et Source Link

Un décompilateur reconstruit du C# à partir de l'IL et des métadonnées, mais des noms comme V_0 et V_1 ont disparu. Voici comment Glass lit les PDB portables (intégrés, à côté du binaire ou récupérés via Source Link) pour restaurer les vrais noms de variables locales et de paramètres, annoter le code décompilé avec le fichier source et la ligne d'origine, et récupérer le code source réel, et comment il se dégrade proprement quand aucun symbole n'existe.

La décompilation reconstruit du C# à partir de deux choses dont le runtime a réellement besoin : l’IL et les métadonnées. Ni l’une ni l’autre ne porte les noms que vous avez saisis pour vos variables locales. L’IL charge et stocke les variables locales par index d’emplacement — ldloc.0, stloc.1 — donc un décompilateur qui ne lit que l’assembly n’a rien pour les nommer. Il invente V_0, num, text. Les noms des paramètres survivent généralement (ils vivent dans la table Param), mais les noms lisibles et porteurs d’intention qui rendent le code navigable — pendingRetries, decodedHeader — ne sont tout simplement pas dans le DLL.

Ils sont dans le PDB. Quand Glass peut apparier un assembly avec ses symboles de débogage, la sortie décompilée cesse de ressembler à une sortie machine et commence à ressembler au code source : vrais noms de variables locales, vraies annotations de fichier source et — avec Source Link — un chemin vers le texte d’origine réel. Cet article traite de ce qui se trouve à l’intérieur d’un PDB portable, des trois manières dont il parvient à Glass, et de la façon dont Glass utilise chaque élément.

Ce que contient réellement un PDB portable

Un PDB portable est un flux de métadonnées dans le même format de tables ECMA-335 qu’un assembly, lu avec System.Reflection.Metadata. Les tables qui comptent pour la décompilation :

  • Document — une ligne par fichier source : le chemin du fichier (C:\src\Acme\OrderService.cs), un hachage et son algorithme, et le GUID de langage. C’est ce qui permet à Glass de dire de quel fichier provient une méthode.
  • MethodDebugInformation — une ligne par corps de méthode, contenant ses points de séquence : un blob qui cartographie les décalages d’IL vers (document, ligne de début, colonne de début, ligne de fin, colonne de fin). Un point de séquence est la jonction entre une instruction IL et un fragment de texte source.
  • LocalScope et LocalVariable — les noms. Un LocalScope couvre une plage semi-ouverte de décalages d’IL et référence une série de lignes LocalVariable ; chaque LocalVariable porte un index d’emplacement et le nom. C’est la table qui retransforme V_0 en pendingRetries.
  • LocalConstant, ImportScope — les constantes visibles pour le débogueur et les directives using dans la portée, utiles pour résoudre les noms de types courts.
  • StateMachineMethodInformation et les informations de débogage personnalisées pour les méthodes async/itérateurs — elles cartographient un MoveNext généré vers la méthode de l’utilisateur, de sorte que les variables locales hissées (désormais des champs d’une struct <Foo>d__3) puissent recevoir leurs noms d’origine.

La lecture de base se présente de la même façon quelle que soit la provenance du PDB :

using System.Reflection.Metadata;

MetadataReader pdb = provider.GetMetadataReader();

foreach (DocumentHandle dh in pdb.Documents)
{
    Document doc = pdb.GetDocument(dh);
    string path = pdb.GetString(doc.Name);           // C:\src\Acme\OrderService.cs
}

// For one method (MethodDefinitionHandle from the assembly reader):
MethodDebugInformation dbg = pdb.GetMethodDebugInformation(methodDebugHandle);
foreach (SequencePoint sp in dbg.GetSequencePoints())
{
    if (sp.IsHidden) continue;                        // the 0xFEEFEE "hidden" marker
    // sp.Offset  -> IL offset
    // sp.StartLine / sp.StartColumn -> source position
    // sp.Document -> which Document row
}

Les noms des variables locales proviennent de l’arbre des portées :

foreach (LocalScopeHandle sh in pdb.GetLocalScopes(methodDefHandle))
{
    LocalScope scope = pdb.GetLocalScope(sh);
    foreach (LocalVariableHandle vh in scope.GetLocalVariables())
    {
        LocalVariable v = pdb.GetLocalVariable(vh);
        int slot = v.Index;                           // matches ldloc/stloc slot
        string name = pdb.GetString(v.Name);          // "pendingRetries"
    }
}

Glass construit une carte emplacement-vers-nom par portée et l’injecte dans la passe de nommage qui, sinon, synthétiserait V_n.

Trois manières dont le PDB parvient à Glass

PDB intégré (entrée du répertoire de débogage de type 17)

Le répertoire de débogage du PE peut porter une entrée de type EmbeddedPortablePdb (valeur 17). Sa charge utile est le flux de PDB portable compressé avec Deflate derrière un petit en-tête (magic MPDB + taille décompressée). Glass le trouve via le lecteur de PE et transmet les octets décompressés directement à un fournisseur de métadonnées :

using var pe = new PEReader(File.OpenRead("Acme.dll"));
foreach (DebugDirectoryEntry e in pe.ReadDebugDirectory())
{
    if (e.Type != DebugDirectoryEntryType.EmbeddedPortablePdb) continue;
    using MetadataReaderProvider provider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(e);
    MetadataReader pdb = provider.GetMetadataReader();
    // ... read documents, sequence points, locals
}

Un PDB intégré voyage à l’intérieur du DLL, donc les symboles sont disponibles où que l’assembly soit copié. C’est le cas le plus aimable : rien à localiser, rien à apparier.

Fichier .pdb à côté du binaire

La disposition classique : Acme.dll auprès de Acme.pdb. Glass charge le flux directement et le valide par rapport à l’entrée de débogage de somme de contrôle CodeView / PDB de l’assembly afin de ne jamais lier un PDB périmé à un DLL recompilé :

using var fs = File.OpenRead("Acme.pdb");
using MetadataReaderProvider provider =
    MetadataReaderProvider.FromPortablePdbImage(fs.ToImmutableArray());
MetadataReader pdb = provider.GetMetadataReader();

(Si vous détenez déjà les octets — par exemple, un serveur de symboles les a renvoyés — FromMetadataImage lit une image en mémoire de la même façon.) L’id du PDB doit correspondre au GUID et à l’âge du répertoire de débogage de l’assembly ; une discordance signifie que les symboles proviennent d’une autre version et Glass les refuse plutôt que de tout mal nommer.

Quand aucun PDB local n’existe, Glass peut en récupérer un depuis un serveur de symboles en utilisant la signature de PDB de l’assembly, exactement comme le fait Visual Studio. Plus intéressant encore, un PDB portable peut porter un enregistrement Source Link — un blob d’informations de débogage personnalisées dont le contenu est un minuscule document JSON :

{
  "documents": {
    "C:\\src\\Acme\\*": "https://raw.githubusercontent.com/acme/orders/3f9a1c2/*"
  }
}

Chaque clé est un préfixe de chemin source avec un joker * ; la valeur est une URL avec un * correspondant. Pour résoudre un document, Glass prend son Document.Name, trouve le plus long préfixe correspondant et substitue le reste dans l’URL — transformant C:\src\Acme\OrderService.cs en https://raw.githubusercontent.com/acme/orders/3f9a1c2/OrderService.cs, épinglé au commit exact qui a produit le binaire. Avec cela, Glass peut afficher le code source d’origine authentique d’une méthode, et non une reconstruction, pour les dépendances que vous avez le droit de lire.

Comment les éléments alimentent le décompilateur

Le trajet du PE au C# annoté est un petit pipeline. Glass localise les symboles, lit les trois types d’informations et achemine chacun vers une étape distincte de la décompilation.

Image PE Acme.dll IL + metadonnees repertoire de debogage PDB integre (type 17) compresse Deflate, dans le PE .pdb a cote valide par id de PDB Recuperation Source Link JSON documents vers URL brute LocalScope / LocalVariable emplacement → vrai nom Points de sequence offset IL → fichier : ligne Nommage + annotation de lignes C# lisible et annote texte source original (si droit)

Voir la différence

Décompilez une méthode sans symboles et vous obtenez du code correct mais anonyme :

// glass decompile Acme.dll --type Acme.Orders.OrderService --member Retry
public bool Retry(Order V_0, int V_1)
{
    int num = 0;
    while (num < V_1)
    {
        if (TrySubmit(V_0))
            return true;
        num++;
    }
    return false;
}

Pointez maintenant Glass vers les symboles. L’option --pdb prend un chemin explicite, --use-embedded lit une entrée de type 17 et --source-link active la résolution d’URL :

glass decompile Acme.dll --type Acme.Orders.OrderService --member Retry \
    --pdb Acme.pdb --source-link --annotate-lines
// Acme.Orders.OrderService  —  C:\src\Acme\OrderService.cs
public bool Retry(Order order, int maxAttempts)
{
    int attempt = 0;                       // line 42
    while (attempt < maxAttempts)          // line 43
    {
        if (TrySubmit(order))              // line 45
            return true;
        attempt++;                         // line 47
    }
    return false;                          // line 49
}

Le paramètre order, les variables locales attempt et maxAttempts, le chemin du fichier et les annotations par ligne proviennent tous du PDB : les noms LocalVariable pour les identifiants, les points de séquence pour les commentaires // line N et la ligne Document pour l’en-tête. Avec --source-link, Glass résout OrderService.cs par rapport à la carte documents et peut afficher le corps d’origine authentique à côté de la reconstruction.

Quand les symboles sont ténus — dégrader proprement

Les PDB sont une optimisation, pas une garantie, et Glass est honnête sur cet écart :

  • Aucun PDB (assembly dépouillé). Rien à lire. Glass synthétise des noms lisibles (order, attempt, count) à partir des types et de l’usage, et omet entièrement les annotations de ligne au lieu d’imprimer de faux numéros de ligne.
  • Version optimisée avec PDB. Le compilateur orienté JIT hisse, réutilise et élimine les variables locales, de sorte qu’un seul emplacement peut sous-tendre plusieurs variables source et les points de séquence peuvent être clairsemés — des expressions entières se réduisent à un seul point, ou apparaissent comme le marqueur masqué 0xFEEFEE (SequencePoint.IsHidden). Glass n’annote que les décalages dont il peut prouver qu’ils correspondent à de vrais fragments source et laisse les régions optimisées non annotées au lieu de deviner.
  • PDB non concordant. Si l’id du PDB ne correspond pas au GUID et à l’âge du répertoire de débogage de l’assembly, Glass le refuse. Un PDB d’une autre version attacherait des noms plausibles mais faux, ce qui est pire que V_0.
  • Async et itérateurs. Les noms des variables locales hissées vivent dans les informations de débogage de la machine à états ; Glass cartographie les champs de <Retry>d__4 vers leurs noms source quand ces informations sont présentes, et se rabat sur des heuristiques sur les noms de champs quand elles ne le sont pas.

La limite honnête

Les symboles récupèrent des noms et des positions, jamais la sémantique que le compilateur a écartée. Les optimisations sont à sens unique : une méthode inlinée ne laisse aucun site d’appel à réétiqueter, une constante repliée n’a pas d’expression d’origine, un emplacement réutilisé ne peut pas être redivisé en deux variables avec certitude. Un PDB rend le code décompilé lisible ; il n’inverse pas le compilateur. Utilisez-le là où il a sa place — déboguer du code que vous possédez, inspecter une dépendance que vous êtes autorisé à lire, reconstruire une trace de pile lors d’un incident — et Glass vous donnera de vrais noms et de vrais numéros de ligne partout où les symboles peuvent les prouver, et du code synthétisé propre partout où ils ne le peuvent pas.

Glass est gratuit. Téléchargez-le, pointez-le vers un assembly et son PDB, et lisez votre propre code tel que vous l’avez écrit.

Essayez Nebula.NET

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