Echte Variablennamen, Quelldateien und Zeilennummern aus PDBs und Source Link wiederherstellen
Ein Decompiler rekonstruiert C# aus IL und Metadaten, doch Namen wie V_0 und V_1 sind verloren. So liest Glass portable PDBs (eingebettet, daneben liegend oder über Source Link bezogen), um echte Namen lokaler Variablen und Parameter wiederherzustellen, dekompilierten Code mit Originalquelldatei und -zeile zu annotieren und den tatsächlichen Quellcode abzurufen, und wie es elegant abbaut, wenn keine Symbole vorhanden sind.
Dekompilation rekonstruiert C# aus zwei Dingen, die die Laufzeit tatsächlich braucht: IL und Metadaten. Keines von beiden trägt die Namen, die Sie für Ihre lokalen Variablen eingegeben haben. Die IL lädt und speichert lokale Variablen über den Slot-Index — ldloc.0, stloc.1 — ein Decompiler, der nur das Assembly liest, hat also nichts, um sie zu benennen. Er erfindet V_0, num, text. Parameternamen überleben meist (sie liegen in der Tabelle Param), doch die lesbaren, absichtstragenden Namen, die Code navigierbar machen — pendingRetries, decodedHeader — sind schlicht nicht in der DLL.
Sie sind im PDB. Wenn Glass ein Assembly mit seinen Debugsymbolen paaren kann, hört die dekompilierte Ausgabe auf, wie Maschinenausgabe auszusehen, und beginnt, wie der Quellcode auszusehen: echte Namen lokaler Variablen, echte Quelldatei-Annotationen und — mit Source Link — ein Pfad zum tatsächlichen Originaltext. Dieser Beitrag handelt davon, was in einem portablen PDB steckt, auf welche drei Weisen es zu Glass gelangt und wie Glass jedes Stück verwendet.
Was ein portables PDB tatsächlich enthält
Ein portables PDB ist ein Metadatenstrom im selben ECMA-335-Tabellenformat wie ein Assembly, gelesen mit System.Reflection.Metadata. Die für die Dekompilation entscheidenden Tabellen:
- Document — eine Zeile pro Quelldatei: der Dateipfad (
C:\src\Acme\OrderService.cs), ein Hash und sein Algorithmus sowie die Sprach-GUID. Das lässt Glass sagen, aus welcher Datei eine Methode stammt. - MethodDebugInformation — eine Zeile pro Methodenrumpf, die seine Sequenzpunkte enthält: ein Blob, der IL-Offsets auf (Dokument, Startzeile, Startspalte, Endzeile, Endspalte) abbildet. Ein Sequenzpunkt ist die Verbindung zwischen einer IL-Anweisung und einem Abschnitt des Quelltextes.
- LocalScope und LocalVariable — die Namen. Ein
LocalScopedeckt einen halboffenen Bereich von IL-Offsets ab und verweist auf eine Folge vonLocalVariable-Zeilen; jedeLocalVariableträgt einen Slot-Index und den Namen. Das ist die Tabelle, dieV_0wieder inpendingRetriesverwandelt. - LocalConstant, ImportScope — für den Debugger sichtbare Konstanten und die
using-Direktiven im Geltungsbereich, nützlich zum Auflösen kurzer Typnamen. - StateMachineMethodInformation und die benutzerdefinierten Debuginformationen für async-/Iterator-Methoden — sie bilden ein generiertes
MoveNextzurück auf die Benutzermethode ab, sodass hochgezogene lokale Variablen (nun Felder einer Struct<Foo>d__3) ihre ursprünglichen Namen erhalten können.
Das Kernauslesen sieht gleich aus, unabhängig davon, woher das PDB stammt:
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
}
Die Namen lokaler Variablen stammen aus dem Geltungsbereichsbaum:
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 baut pro Geltungsbereich eine Slot-zu-Name-Karte und speist sie in den Benennungsdurchlauf ein, der sonst V_n synthetisieren würde.
Drei Wege, auf denen das PDB zu Glass gelangt
Eingebettetes PDB (Debug-Directory-Eintrag vom Typ 17)
Das Debug-Directory des PE kann einen Eintrag vom Typ EmbeddedPortablePdb (Wert 17) tragen. Seine Nutzlast ist der portable PDB-Strom, mit Deflate komprimiert hinter einem kleinen Header (Magic MPDB + unkomprimierte Größe). Glass findet ihn über den PE-Reader und übergibt die entpackten Bytes direkt an einen Metadaten-Provider:
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
}
Ein eingebettetes PDB reist innerhalb der DLL, also sind die Symbole verfügbar, egal wohin das Assembly kopiert wird. Das ist der freundlichste Fall: nichts zu finden, nichts abzugleichen.
Daneben liegende .pdb-Datei
Das klassische Layout: Acme.dll neben Acme.pdb. Glass lädt den Strom direkt und validiert ihn gegen den CodeView-/PDB-Prüfsummen-Debugeintrag des Assemblys, damit nie ein veraltetes PDB an eine neu gebaute DLL gebunden wird:
using var fs = File.OpenRead("Acme.pdb");
using MetadataReaderProvider provider =
MetadataReaderProvider.FromPortablePdbImage(fs.ToImmutableArray());
MetadataReader pdb = provider.GetMetadataReader();
(Wenn Sie die Bytes bereits halten — etwa, weil ein Symbolserver sie zurückgab — liest FromMetadataImage ein In-Memory-Image auf dieselbe Weise.) Die PDB-ID muss mit GUID und Alter des Debug-Directorys des Assemblys übereinstimmen; eine Abweichung bedeutet, dass die Symbole aus einem anderen Build stammen, und Glass lehnt sie ab, statt alles falsch zu benennen.
Über Source Link / einen Symbolserver bezogen
Wenn kein lokales PDB existiert, kann Glass eines von einem Symbolserver abrufen, unter Verwendung der PDB-Signatur des Assemblys, genau wie Visual Studio es tut. Interessanter noch: Ein portables PDB kann einen Source-Link-Datensatz tragen — einen Blob benutzerdefinierter Debuginformationen, dessen Inhalt ein winziges JSON-Dokument ist:
{
"documents": {
"C:\\src\\Acme\\*": "https://raw.githubusercontent.com/acme/orders/3f9a1c2/*"
}
}
Jeder Schlüssel ist ein Quellpfad-Präfix mit einem *-Platzhalter; der Wert ist eine URL mit einem passenden *. Um ein Dokument aufzulösen, nimmt Glass seinen Document.Name, findet das längste passende Präfix und setzt den Rest in die URL ein — es verwandelt C:\src\Acme\OrderService.cs in https://raw.githubusercontent.com/acme/orders/3f9a1c2/OrderService.cs, gepinnt an den exakten Commit, der die Binärdatei erzeugt hat. Damit kann Glass den echten Originalquellcode einer Methode anzeigen, keine Rekonstruktion — für Abhängigkeiten, die Sie lesen dürfen.
Wie die Teile den Decompiler speisen
Der Weg vom PE zum annotierten C# ist eine kleine Pipeline. Glass lokalisiert die Symbole, liest die drei Informationsarten und leitet jede in eine andere Stufe der Dekompilation.
Den Unterschied sehen
Dekompilieren Sie eine Methode ohne Symbole, erhalten Sie korrekten, aber anonymen Code:
// 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;
}
Weisen Sie Glass nun auf die Symbole. Die Option --pdb nimmt einen expliziten Pfad, --use-embedded liest einen Eintrag vom Typ 17 und --source-link aktiviert die URL-Auflösung:
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
}
Der Parameter order, die lokalen Variablen attempt und maxAttempts, der Dateipfad und die zeilenweisen Annotationen stammen alle aus dem PDB: die LocalVariable-Namen für die Bezeichner, die Sequenzpunkte für die // line N-Kommentare und die Document-Zeile für den Kopf. Mit --source-link löst Glass OrderService.cs gegen die documents-Karte auf und kann den echten Originalrumpf neben der Rekonstruktion anzeigen.
Wenn die Symbole dünn sind — elegant abbauen
PDBs sind eine Optimierung, keine Garantie, und Glass ist ehrlich über die Lücke:
- Gar kein PDB (gestripptes Assembly). Nichts zu lesen. Glass synthetisiert lesbare Namen (
order,attempt,count) aus Typen und Verwendung und lässt Zeilenannotationen ganz weg, statt falsche Zeilennummern zu drucken. - Optimierter Release-Build mit PDB. Der JIT-orientierte Compiler zieht lokale Variablen hoch, verwendet sie wieder und eliminiert sie, sodass ein einzelner Slot mehrere Quellvariablen tragen kann und die Sequenzpunkte spärlich sein können — ganze Ausdrücke fallen auf einen Punkt zusammen oder erscheinen als der verborgene Marker
0xFEEFEE(SequencePoint.IsHidden). Glass annotiert nur die Offsets, von denen es beweisen kann, dass sie auf echte Quellabschnitte abbilden, und lässt optimierte Bereiche unannotiert, statt zu raten. - Nicht übereinstimmendes PDB. Wenn die PDB-ID nicht zu GUID und Alter des Debug-Directorys des Assemblys passt, lehnt Glass es ab. Ein PDB aus einem anderen Build würde plausible, aber falsche Namen anhängen, was schlimmer ist als
V_0. - Async und Iteratoren. Die Namen hochgezogener lokaler Variablen liegen in den Debuginformationen der Zustandsmaschine; Glass bildet die Felder von
<Retry>d__4auf ihre Quellnamen ab, wenn diese Informationen vorhanden sind, und greift auf Heuristiken über die Feldnamen zurück, wenn nicht.
Die ehrliche Grenze
Symbole stellen Namen und Positionen wieder her, nie die Semantik, die der Compiler verworfen hat. Optimierungen sind einseitig: Eine inline eingefügte Methode hinterlässt keine Aufrufstelle zum Neubeschriften, eine gefaltete Konstante hat keinen ursprünglichen Ausdruck, ein wiederverwendeter Slot lässt sich nicht mit Sicherheit in zwei Variablen zurückteilen. Ein PDB macht dekompilierten Code lesbar; es kehrt den Compiler nicht um. Setzen Sie es dort ein, wo es hingehört — eigenen Code debuggen, eine Abhängigkeit inspizieren, die Sie lesen dürfen, einen Stacktrace während eines Vorfalls rekonstruieren — und Glass liefert Ihnen echte Namen und echte Zeilennummern überall dort, wo die Symbole sie beweisen können, und sauberen synthetisierten Code überall dort, wo sie es nicht können.
Glass ist kostenlos. Laden Sie es herunter, weisen Sie es auf ein Assembly und sein PDB, und lesen Sie Ihren eigenen Code so, wie Sie ihn geschrieben haben.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.