Décompiler sans PDB : ce que vous perdez et ce qu'un décompilateur récupère
Une DLL de release est livrée sans son PDB, et pourtant un décompilateur reconstruit du C# lisible. Voici la répartition : ce qui vit dans les métadonnées de l'assembly (et survit), ce qui vit uniquement dans le PDB (noms locaux, numéros de ligne), et comment un décompilateur comble le vide quand les symboles manquent.
Un décompilateur ouvre une DLL de release livrée sans aucun .pdb à côté, et il en sort du C# lisible. Cela surprend ceux qui supposent que les symboles sont nécessaires pour rétro-concevoir un assembly .NET. Ils ne le sont pas — et comprendre pourquoi vous dit exactement ce que vous gagnez quand un PDB est présent, ce qui compte que vous analysiez le binaire de quelqu”un d”autre ou que vous symbolisiez votre propre trace de pile de production.
Deux fichiers, deux rôles
Une compilation .NET produit deux artefacts. L”assembly (.dll/.exe) porte le programme : les métadonnées — un ensemble de tables décrivant chaque type, méthode, champ, propriété et leurs signatures — et l”IL (les corps de méthode compilés). Le PDB (.pdb) porte l”information de débogage sur cet assembly : il n”est pas nécessaire pour exécuter le programme, seulement pour le déboguer confortablement.
Le point crucial est de savoir ce qui vit où. Tout ce dont un décompilateur a besoin pour reconstruire la logique est dans l”assembly. Le PDB contient un ensemble spécifique et plus petit de choses que l”assembly omet délibérément.
Ce qui survit dans l’assembly (sans PDB)
- Noms de types, méthodes, champs et propriétés — pour tout ce qui n”est pas obfusqué. Les métadonnées stockent
CustomerService,ChargeAsync,_repositorytels quels ; le décompilateur les lit directement. - Signatures complètes — types de paramètres et de retour, génériques, modificateurs personnalisés. Le système de types est reconstruit exactement.
- Corps de méthode — l”IL, à partir duquel le décompilateur reconstruit le flux de contrôle, les expressions et la syntaxe C#.
- Noms de paramètres — ceux-ci sont dans les métadonnées (la table
Param), donc les paramètres des méthodes gardent leurs vrais noms même sans PDB. - Attributs, constantes, membres d”enum, noms de ressources — tout est métadonnées.
Ainsi, une décompilation sans PDB vous donne un code correct, bien nommé et entièrement typé. L”écart est plus étroit que la plupart ne le pensent.
Ce que seul le PDB possède
Deux choses que l”assembly ne stocke pas :
Noms des variables locales. Les métadonnées gardent le type et le slot de chaque locale, dans le StandAloneSig de la méthode, mais pas son nom. L”IL se réfère aux locales par index — ldloc.0, stloc.1 — jamais par nom :
// IL for: int total = price * qty;
ldarg.1 // price
ldarg.2 // qty
mul
stloc.0 // 'total' — but the name "total" is nowhere in the assembly
Points de séquence. Le PDB mappe chaque offset IL vers un fichier source, une ligne et une colonne. C”est ce qui transforme une trace de pile en at CustomerService.ChargeAsync() in CustomerService.cs:line 42, et ce qui permet à un débogueur de surligner la bonne ligne source à mesure que vous avancez. Pas de PDB, pas de numéros de ligne : les exceptions portent toujours la méthode, mais pas l”emplacement en son sein.
Comment un décompilateur comble le vide
Face à une méthode dont les locales n”ont pas de nom, un décompilateur les synthétise à partir du type de chaque locale. Un int devient num (puis num2, num3), un string devient text, un bool devient flag, un tableau devient array, un compteur de boucle devient i. Ainsi, la décompilation sans PDB de notre extrait se lit :
int num = price * qty; // was 'total'
Correct et clair — simplement pas le nom d”origine. Les noms de paramètres survivent (ils sont dans les métadonnées), donc la signature et les arguments se lisent naturellement ; seules les locales sont reconstruites.
Fournissez le PDB correspondant et le décompilateur utilise les vrais noms locaux et peut afficher les numéros de ligne, car il a maintenant les points de séquence. Glass.NET charge automatiquement un .pdb adjacent lorsqu”il est à côté de l”assembly, et lit un PDB intégré directement depuis le PE lorsque la compilation a choisi DebugType=embedded — donc vous obtenez souvent les vrais noms sans aucun fichier supplémentaire.
Les PDB dans le .NET moderne
Deux détails comptent quand vous partez à la recherche de symboles :
- Le PDB portable est le format multiplateforme actuel (bien plus petit que l”ancien PDB Windows). Il peut vivre comme un
.pdbséparé, ou être intégré dans l”assembly (<DebugType>embedded</DebugType>), ce qui signifie que les symboles voyagent avec la DLL et qu”un décompilateur les lit sans rien d”autre à localiser. - SourceLink va un cran plus loin : le PDB enregistre une URL pour chaque fichier source, de sorte qu”un débogueur ou un décompilateur peut récupérer le source d”origine à la demande. Avec SourceLink, vous ne lisez pas du tout du C# reconstruit : vous lisez le vrai.
La conclusion pratique : avant de supposer que vous êtes coincé avec des noms synthétisés, vérifiez si l”assembly a un PDB intégré, et si un .pdb portable a été livré à côté. Souvent, les symboles sont plus proches que vous ne le pensez.
Pourquoi cela compte dans les deux cas
Si vous analysez un binaire tiers, cela vous dit d”attendre une logique précise et de vrais noms publics, avec des locales reconstruites — et de récupérer tout PDB disponible pour retrouver les noms d”origine. Si vous publiez un logiciel, c”est un rappel qu”un assembly expose déjà vos noms de types et de méthodes à quiconque dispose d”un décompilateur, PDB ou non ; les symboles n”ajoutent que les noms locaux et les numéros de ligne par-dessus. Garder les vrais noms hors d”une build de release — le travail de l”obfuscation — est une décision distincte de celle de livrer ou non un PDB. Les deux contrôlent des choses différentes, et vous savez désormais exactement lesquelles.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.