Skip to content
← Todas las publicaciones
· Delta1 Labs Decompilación.NETGuía

Decompilar sin PDB: qué pierdes y qué recupera un decompilador

Una DLL de release se distribuye sin su PDB y, aun así, un decompilador reconstruye C# legible. Esta es la separación: qué vive en los metadatos del ensamblado (y sobrevive), qué vive solo en el PDB (nombres locales, números de línea), y cómo un decompilador rellena el hueco cuando faltan los símbolos.

Un decompilador abre una DLL de release que se distribuyó sin ningún .pdb al lado, y de ahí sale C# legible. Eso sorprende a quienes suponen que los símbolos son necesarios para hacer ingeniería inversa de un ensamblado .NET. No lo son, y entender por qué te dice exactamente qué ganas cuando sí hay un PDB, lo que importa tanto si analizas el binario de otra persona como si simbolizas tu propia traza de pila de producción.

Dos archivos, dos trabajos

Una compilación de .NET produce dos artefactos. El ensamblado (.dll/.exe) lleva el programa: los metadatos —un conjunto de tablas que describen cada tipo, método, campo, propiedad y sus firmas— y el IL (los cuerpos de método compilados). El PDB (.pdb) lleva información de depuración sobre ese ensamblado: no se necesita para ejecutar el programa, solo para depurarlo cómodamente.

El punto crucial es qué vive dónde. Todo lo que un decompilador necesita para reconstruir la lógica está en el ensamblado. El PDB guarda un conjunto específico y más pequeño de cosas que el ensamblado omite deliberadamente.

Qué sobrevive en el ensamblado (sin necesidad de PDB)

  • Nombres de tipos, métodos, campos y propiedades —para cualquier cosa no ofuscada—. Los metadatos guardan CustomerService, ChargeAsync, _repository tal cual; el decompilador los lee directamente.
  • Firmas completas —tipos de parámetros y de retorno, genéricos, modificadores personalizados—. El sistema de tipos se reconstruye exactamente.
  • Cuerpos de método —el IL, a partir del cual el decompilador reconstruye el flujo de control, las expresiones y la sintaxis de C#.
  • Nombres de parámetros —estos sí están en los metadatos (la tabla Param), así que los parámetros de los métodos conservan sus nombres reales incluso sin PDB.
  • Atributos, constantes, miembros de enum, nombres de recursos —todo son metadatos.

Así que una decompilación sin PDB te da código correcto, bien nombrado y completamente tipado. El hueco es más estrecho de lo que la mayoría espera.

Qué tiene solo el PDB

Dos cosas que el ensamblado no almacena:

Nombres de variables locales. Los metadatos guardan el tipo y la ranura de cada local, en el StandAloneSig del método, pero no su nombre. El IL se refiere a las locales por índice —ldloc.0, stloc.1— nunca por nombre:

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

Puntos de secuencia. El PDB mapea cada offset de IL a un archivo fuente, línea y columna. Esto es lo que convierte una traza de pila en at CustomerService.ChargeAsync() in CustomerService.cs:line 42, y lo que permite a un depurador resaltar la línea fuente correcta mientras avanzas. Sin PDB, sin números de línea: las excepciones aún llevan el método, pero no la ubicación dentro de él.

Ensamblado (.dll)metadatos + IL — siempre presentesPDB (.pdb) — opcionalnombres locales + números de líneaDecompiladorC# legible

Cómo rellena el hueco un decompilador

Ante un método cuyas locales no tienen nombre, un decompilador los sintetiza a partir del tipo de cada local. Un int se convierte en num (luego num2, num3), un string en text, un bool en flag, un array en array, un contador de bucle en i. Así que la decompilación sin PDB de nuestro fragmento se lee:

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

Correcto y claro, solo que no es el nombre original. Los nombres de parámetros sobreviven (están en los metadatos), así que la firma y los argumentos se leen con naturalidad; solo las locales se reconstruyen.

Proporciona el PDB correspondiente y el decompilador usa los nombres locales reales y puede mostrar números de línea, porque ahora tiene los puntos de secuencia. Glass.NET carga un .pdb adyacente automáticamente cuando está junto al ensamblado, y lee un PDB incrustado directamente del PE cuando la compilación eligió DebugType=embedded, así que a menudo obtienes los nombres reales sin ningún archivo extra.

PDB en el .NET moderno

Dos detalles importan cuando vas a buscar símbolos:

  • PDB portable es el formato multiplataforma actual (mucho más pequeño que el PDB de Windows heredado). Puede vivir como un .pdb separado, o estar incrustado dentro del ensamblado (<DebugType>embedded</DebugType>), lo que significa que los símbolos viajan con la DLL y un decompilador los lee sin nada extra que localizar.
  • SourceLink va un paso más allá: el PDB registra una URL para cada archivo fuente, de modo que un depurador o decompilador puede obtener el fuente original bajo demanda. Con SourceLink no estás leyendo C# reconstruido en absoluto: estás leyendo lo real.

La conclusión práctica: antes de suponer que estás atascado con nombres sintetizados, comprueba si el ensamblado tiene un PDB incrustado, y si un .pdb portable se distribuyó junto a él. A menudo los símbolos están más cerca de lo que crees.

Por qué importa en cualquier caso

Si estás analizando un binario de terceros, esto te dice que esperes lógica precisa y nombres públicos reales, con locales reconstruidas, y que consigas cualquier PDB disponible para recuperar los nombres originales. Si estás publicando software, es un recordatorio de que un ensamblado ya expone tus nombres de tipos y métodos a cualquiera con un decompilador, con PDB o sin él; los símbolos solo añaden nombres locales y números de línea encima. Mantener los nombres reales fuera de una compilación de release —el trabajo de la ofuscación— es una decisión aparte de si distribuyes un PDB. Las dos controlan cosas distintas, y ahora sabes exactamente cuál.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.