Skip to content
← All posts
· Delta1 Labs Decompilation.NETGuide

Decompiling Without a PDB: What You Lose and What a Decompiler Recovers

A release DLL ships without its PDB, yet a decompiler still reconstructs readable C#. Here is the split: what lives in the assembly metadata (and survives), what lives only in the PDB (local names, line numbers), and how a decompiler fills the gap when the symbols are missing.

A decompiler opens a release DLL that shipped with no .pdb next to it, and out comes readable C#. That surprises people who assume symbols are required to reverse a .NET assembly. They are not — and understanding why tells you exactly what you gain when a PDB is present, which matters whether you are analyzing someone else’s binary or symbolicating your own production stack trace.

Two files, two jobs

A .NET build produces two artifacts. The assembly (.dll/.exe) carries the program: metadata — a set of tables describing every type, method, field, property and their signatures — and the IL (the compiled method bodies). The PDB (.pdb) carries debugging information about that assembly: it is not needed to run the program, only to debug it comfortably.

The crucial point is what lives where. Everything a decompiler needs to reconstruct logic is in the assembly. The PDB holds a specific, smaller set of things that the assembly deliberately omits.

What survives in the assembly (no PDB needed)

  • Type, method, field and property names — for anything not obfuscated. Metadata stores CustomerService, ChargeAsync, _repository verbatim; the decompiler reads them straight out.
  • Full signatures — parameter and return types, generics, custom modifiers. The type system is reconstructed exactly.
  • Method bodies — the IL, from which the decompiler rebuilds control flow, expressions and C# syntax.
  • Parameter names — these are in metadata (the Param table), so method parameters keep their real names even without a PDB.
  • Attributes, constants, enum members, resource names — all metadata.

So a no-PDB decompile gives you correct, well-named, fully-typed code. The gap is narrower than most expect.

What only the PDB has

Two things the assembly does not store:

Local variable names. The metadata keeps each local’s type and slot, in the method’s StandAloneSig, but not its name. The IL refers to locals by index — ldloc.0, stloc.1 — never by name:

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

Sequence points. The PDB maps each IL offset to a source file, line and column. This is what turns a stack trace into at CustomerService.ChargeAsync() in CustomerService.cs:line 42, and what lets a debugger highlight the right source line as you step. No PDB, no line numbers — exceptions still carry the method, but not the location within it.

Assembly (.dll)metadata + IL — always presentPDB (.pdb) — optionallocal names + line numbersDecompilerreadable C#

How a decompiler fills the gap

Faced with a method whose locals have no names, a decompiler synthesizes them from each local’s type. An int becomes num (then num2, num3), a string becomes text, a bool becomes flag, an array becomes array, a loop counter becomes i. So the no-PDB decompile of our snippet reads:

int num = price * qty;   // was 'total'

Correct and clear — just not the original name. Parameter names survive (they are in metadata), so the signature and arguments read naturally; only the locals are reconstructed.

Supply the matching PDB and the decompiler uses the real local names and can show line numbers, because now it has the sequence points. Glass.NET loads a sidecar .pdb automatically when it sits next to the assembly, and reads an embedded PDB straight out of the PE when the build chose DebugType=embedded — so you often get the real names with no extra file at all.

PDBs in modern .NET

Two details matter when you go looking for symbols:

  • Portable PDB is the current cross-platform format (far smaller than the legacy Windows PDB). It can live as a separate .pdb, or be embedded inside the assembly (<DebugType>embedded</DebugType>), which means the symbols travel with the DLL and a decompiler reads them with nothing extra to locate.
  • SourceLink goes one step further: the PDB records a URL for each source file, so a debugger or decompiler can fetch the original source on demand. With SourceLink you are not reading reconstructed C# at all — you are reading the real thing.

The practical upshot: before assuming you are stuck with synthesized names, check whether the assembly has an embedded PDB, and whether a portable .pdb shipped alongside it. Often the symbols are closer than you think.

Why this matters either way

If you are analyzing a third-party binary, this tells you to expect accurate logic and real public names, with reconstructed locals — and to grab any available PDB to get the original names back. If you are shipping software, it is a reminder that an assembly already exposes your type and method names to anyone with a decompiler, PDB or not; symbols only add local names and line numbers on top. Keeping real names out of a release build — the job of obfuscation — is a separate decision from whether you ship a PDB. The two control different things, and now you know exactly which.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.