Skip to content
← Todas las publicaciones
· Delta1 Labs PDBGlass

Recuperar nombres reales de variables, archivos fuente y números de línea desde PDB y Source Link

Un descompilador reconstruye C# a partir del IL y los metadatos, pero nombres como V_0 y V_1 se han perdido. Así es como Glass lee los PDB portables (incrustados, adyacentes u obtenidos vía Source Link) para restaurar los nombres reales de variables locales y parámetros, anotar el código descompilado con el archivo fuente y la línea originales, y recuperar el código fuente real, y cómo se degrada con elegancia cuando no hay símbolos.

La descompilación reconstruye C# a partir de dos cosas que el runtime realmente necesita: IL y metadatos. Ninguna lleva los nombres que escribiste para tus variables locales. El IL carga y almacena las variables locales por índice de ranura —ldloc.0, stloc.1— así que un descompilador que lee solo el ensamblado no tiene con qué nombrarlas. Inventa V_0, num, text. Los nombres de los parámetros suelen sobrevivir (viven en la tabla Param), pero los nombres legibles y portadores de intención que hacen que el código sea navegable —pendingRetries, decodedHeader— simplemente no están en el DLL.

Están en el PDB. Cuando Glass puede emparejar un ensamblado con sus símbolos de depuración, la salida descompilada deja de parecer salida de máquina y empieza a parecer el código fuente: nombres reales de variables locales, anotaciones reales de archivo fuente y —con Source Link— una ruta al texto original real. Esta entrada trata de lo que hay dentro de un PDB portable, las tres maneras en que llega a Glass y cómo Glass usa cada pieza.

Qué contiene en realidad un PDB portable

Un PDB portable es un flujo de metadatos en el mismo formato de tablas ECMA-335 que un ensamblado, que se lee con System.Reflection.Metadata. Las tablas que importan para la descompilación:

  • Document — una fila por archivo fuente: la ruta del archivo (C:\src\Acme\OrderService.cs), un hash y su algoritmo, y el GUID de lenguaje. Esto es lo que permite a Glass decir de qué archivo proviene un método.
  • MethodDebugInformation — una fila por cuerpo de método, que contiene sus puntos de secuencia: un blob que mapea desplazamientos de IL a (documento, línea inicial, columna inicial, línea final, columna final). Un punto de secuencia es la unión entre una instrucción de IL y un fragmento de texto fuente.
  • LocalScope y LocalVariable — los nombres. Un LocalScope cubre un rango semiabierto de desplazamientos de IL y referencia una serie de filas LocalVariable; cada LocalVariable lleva un índice de ranura y el nombre. Esta es la tabla que convierte V_0 de nuevo en pendingRetries.
  • LocalConstant, ImportScope — constantes visibles para el depurador y las directivas using en ámbito, útiles para resolver nombres de tipos cortos.
  • StateMachineMethodInformation y la información de depuración personalizada para métodos async/iteradores — estos mapean un MoveNext generado de vuelta al método del usuario, de modo que las variables locales elevadas (ahora campos en una struct <Foo>d__3) puedan recibir sus nombres originales.

La lectura central se ve igual sin importar de dónde provenga el 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
}

Los nombres de las variables locales provienen del árbol de ámbitos:

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 construye un mapa de ranura a nombre por ámbito y lo introduce en la fase de nombrado que de otro modo sintetizaría V_n.

Tres maneras en que el PDB llega a Glass

PDB incrustado (entrada del directorio de depuración de tipo 17)

El directorio de depuración del PE puede llevar una entrada de tipo EmbeddedPortablePdb (valor 17). Su carga útil es el flujo de PDB portable comprimido con Deflate tras una pequeña cabecera (magic MPDB + tamaño sin comprimir). Glass lo encuentra mediante el lector del PE y pasa los bytes descomprimidos directamente a un proveedor de metadatos:

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 incrustado viaja dentro del DLL, así que los símbolos están disponibles sin importar a dónde se copie el ensamblado. Este es el caso más amable: nada que localizar, nada que emparejar.

Archivo .pdb adyacente

La disposición clásica: Acme.dll junto a Acme.pdb. Glass carga el flujo directamente y lo valida contra la entrada de depuración de suma de verificación CodeView / PDB del ensamblado para no asociar nunca un PDB obsoleto a un DLL recompilado:

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

(Si ya tienes los bytes —por ejemplo, un servidor de símbolos los devolvió— FromMetadataImage lee una imagen en memoria del mismo modo.) El id del PDB debe coincidir con el GUID y la edad del directorio de depuración del ensamblado; una discrepancia significa que los símbolos son de otra compilación y Glass los rechaza en lugar de nombrar mal todo.

Cuando no existe un PDB local, Glass puede recuperar uno de un servidor de símbolos usando la firma de PDB del ensamblado, exactamente como hace Visual Studio. Más interesante aún, un PDB portable puede llevar un registro de Source Link —un blob de información de depuración personalizada cuyo contenido es un diminuto documento JSON:

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

Cada clave es un prefijo de ruta fuente con un comodín *; el valor es una URL con un * correspondiente. Para resolver un documento, Glass toma su Document.Name, encuentra el prefijo coincidente más largo y sustituye el resto en la URL —convirtiendo C:\src\Acme\OrderService.cs en https://raw.githubusercontent.com/acme/orders/3f9a1c2/OrderService.cs, fijado al commit exacto que produjo el binario. Con eso, Glass puede mostrar el código fuente original genuino de un método, no una reconstrucción, para las dependencias que tengas derecho a leer.

Cómo alimentan las piezas al descompilador

El recorrido del PE al C# anotado es una pequeña tubería. Glass localiza los símbolos, lee los tres tipos de información y encamina cada uno a una fase distinta de la descompilación.

Imagen PE Acme.dll IL + metadatos directorio de depuracion PDB incrustado (tipo 17) comprimido con Deflate, en el PE .pdb adyacente validado por id de PDB Recuperacion Source Link JSON documents a URL en crudo LocalScope / LocalVariable ranura → nombre real Puntos de secuencia offset IL → archivo : linea Nombrado + anotacion de lineas C# legible y anotado texto fuente original (si hay derecho)

Ver la diferencia

Descompila un método sin símbolos y obtienes código correcto pero anónimo:

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

Ahora apunta Glass a los símbolos. La opción --pdb toma una ruta explícita, --use-embedded lee una entrada de tipo 17 y --source-link habilita la resolución de 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
}

El parámetro order, las variables locales attempt y maxAttempts, la ruta del archivo y las anotaciones por línea provienen todas del PDB: los nombres de LocalVariable para los identificadores, los puntos de secuencia para los comentarios // line N y la fila Document para la cabecera. Con --source-link, Glass resuelve OrderService.cs contra el mapa documents y puede mostrar el cuerpo original genuino junto a la reconstrucción.

Cuando los símbolos son escasos: degradar con elegancia

Los PDB son una optimización, no una garantía, y Glass es honesto respecto a esa brecha:

  • Ningún PDB (ensamblado despojado). Nada que leer. Glass sintetiza nombres legibles (order, attempt, count) a partir de los tipos y el uso, y omite por completo las anotaciones de línea en lugar de imprimir números de línea falsos.
  • Compilación de versión optimizada con PDB. El compilador orientado al JIT eleva, reutiliza y elimina variables locales, así que una sola ranura puede respaldar varias variables fuente y los puntos de secuencia pueden ser escasos —expresiones enteras se colapsan en un solo punto, o aparecen como el marcador oculto 0xFEEFEE (SequencePoint.IsHidden). Glass anota solo los desplazamientos que puede probar que mapean a fragmentos fuente reales y deja sin anotar las regiones optimizadas en lugar de adivinar.
  • PDB no coincidente. Si el id del PDB no coincide con el GUID y la edad del directorio de depuración del ensamblado, Glass lo rechaza. Un PDB de otra compilación adjuntaría nombres plausibles pero falsos, lo cual es peor que V_0.
  • Async e iteradores. Los nombres de las variables locales elevadas viven en la información de depuración de la máquina de estados; Glass mapea los campos de <Retry>d__4 de vuelta a sus nombres fuente cuando esa información está presente, y recurre a heurísticas sobre los nombres de campo cuando no lo está.

El límite honesto

Los símbolos recuperan nombres y posiciones, nunca la semántica que el compilador descartó. Las optimizaciones son de un solo sentido: un método en línea no deja un sitio de llamada que reetiquetar, una constante plegada no tiene expresión original, una ranura reutilizada no puede dividirse de vuelta en dos variables con certeza. Un PDB hace legible el código descompilado; no revierte el compilador. Úsalo donde corresponde —depurar código que posees, inspeccionar una dependencia que tienes licencia para leer, reconstruir un rastro de pila durante un incidente— y Glass te dará nombres reales y números de línea reales allí donde los símbolos puedan probarlos, y código sintetizado limpio allí donde no.

Glass es gratuito. Descárgalo, apúntalo a un ensamblado y su PDB, y lee tu propio código tal como lo escribiste.

Prueba Nebula.NET

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