Skip to content
← Todas las publicaciones
· Delta1 Labs ILGlass

Leer la vista IL y navegar por los tokens de metadatos en Glass

Una explicación a fondo sobre cómo cambiar a la vista IL en Glass, decodificar tokens de metadatos (0x06 MethodDef, 0x0A MemberRef, 0x70 UserString), leer la cabecera del cuerpo de un método y hacer clic en un token para saltar a su definición.

El C# que te muestra un decompilador es una reconstrucción. Normalmente es fiel, pero es una interpretación de algo más preciso: el Lenguaje Intermedio (IL) que el compilador emitió realmente, más las tablas de metadatos que dan significado a cada nombre, tipo y firma. Cuando la reconstrucción es limpia nunca necesitas mirar por debajo. Cuando es ambigua —una sobrecarga que no logras fijar, una conversión de boxing que desapareció, un bucle cuya forma no coincide con el código que esperabas— la vista IL es la verdad de base, y los tokens de metadatos son la forma de navegarla.

Este artículo es un recorrido por la vista IL de Glass.NET, el decompilador de .NET gratuito. Tomaremos un método pequeño de C#, veremos su IL, decodificaremos los tokens de metadatos que llevan las instrucciones y veremos cómo al hacer clic en un token en Glass saltas al miembro que nombra. El enfoque, en todo momento, es legítimo: entender tu propio código, auditar una dependencia que distribuyes o trabajar en un incidente sobre un ensamblado del que eres responsable.

Cambiar de la vista C# a la vista IL

Abre un ensamblado en Glass y selecciona un método. Por defecto ves C# reconstruido. Cambia la vista a IL (el conmutador de vista en el panel de código) y el mismo método se vuelve a renderizar como el bytecode que consumirá el JIT. Nada del ensamblado cambia; estás mirando el mismo cuerpo del método a través de una lente de más bajo nivel.

Desde la CLI el equivalente es una opción de decompile:

# C# reconstruido (por defecto)
glass decompile MyApp.dll --type MyApp.Billing.Invoice --member Total

# El mismo cuerpo del método como IL, con los tokens de metadatos mostrados
glass decompile MyApp.dll --type MyApp.Billing.Invoice --member Total --il

La salida de --il imprime cada instrucción con su offset, su opcode y su operando. Cuando un operando es un token de metadatos, Glass imprime el valor crudo de 4 bytes y lo anota con el miembro al que se resuelve, para que puedas leer el cuerpo sin cruzar tablas a mano.

Qué es realmente un token de metadatos

Cada ensamblado lleva un conjunto de tablas de metadatos —MethodDef, TypeDef, MemberRef, Field, etc.—, cada una una lista de filas. Un token de metadatos es simplemente un asa de 4 bytes que nombra una fila:

  • El byte alto es el id de la tabla.
  • Los tres bytes bajos son el índice de fila (empezando en 1) dentro de esa tabla.

Los ids de tabla con los que te encontrarás constantemente:

Id de tablaTablaQué nombra
0x02TypeDefUn tipo definido en este ensamblado
0x01TypeRefUn tipo definido en otro lugar, referenciado aquí
0x06MethodDefUn método definido en este ensamblado
0x0AMemberRefUn método o campo de un tipo externo
0x04FieldUn campo definido en este ensamblado
0x1BTypeSpecUn tipo construido (p. ej. List<int>)
0x11StandAloneSigUna firma independiente (variables locales)
0x70UserStringUn literal de cadena, en el heap #US

Así, el token 0x0A000012 se decodifica como: byte alto 0x0A → tabla MemberRef; tres bytes bajos 0x000012 → fila 18. Nombra el decimoctavo MemberRef: algún método o campo de un tipo de otro ensamblado. El token 0x06000004 es la fila 4 de MethodDef: el cuarto método definido en el ensamblado que estás mirando. Y 0x70000001 es un token al heap UserString en el offset 1, no una fila de tabla en absoluto; más sobre esto abajo.

Cómo llevan tokens las instrucciones IL

Toma este método corriente:

public string Describe(Customer c)
{
    var sb = new StringBuilder();
    sb.Append("Customer: ");
    sb.Append(c.Name);
    return sb.ToString();
}

En la vista IL, Glass muestra algo como esto (tokens anotados en la columna derecha):

.method public instance string Describe(class Customer c) cil managed
{
  .maxstack 2
  .locals init (class [System.Runtime]System.Text.StringBuilder V_0)

  IL_0000:  newobj     instance void [System.Runtime]System.Text.StringBuilder::.ctor()   // 0x0A000011  MemberRef
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  ldstr      "Customer: "                                                        // 0x70000001  UserString
  IL_000c:  callvirt   instance class ...StringBuilder StringBuilder::Append(string)       // 0x0A000012  MemberRef
  IL_0011:  pop
  IL_0012:  ldloc.0
  IL_0013:  ldarg.1
  IL_0014:  callvirt   instance string Customer::get_Name()                                // 0x06000007  MethodDef
  IL_0019:  callvirt   instance class ...StringBuilder StringBuilder::Append(string)       // 0x0A000012  MemberRef
  IL_001e:  pop
  IL_001f:  ldloc.0
  IL_0020:  callvirt   instance string StringBuilder::ToString()                           // 0x0A000013  MemberRef
  IL_0025:  ret
}

Lee los operandos como tokens y las dependencias del método se vuelven explícitas:

  • newobj 0x0A000011 — construir mediante un MemberRef: el constructor de StringBuilder, que vive en otro ensamblado.
  • ldstr 0x70000001 — poner en la pila un literal de cadena. El operando es un token UserString; el texto del literal vive en el heap #US.
  • callvirt 0x0A000012 — llamar a StringBuilder.Append(string) a través de un MemberRef. Fíjate en que el mismo token aparece dos veces: ambas llamadas a Append enlazan a la única sobrecarga Append(string), que es exactamente el tipo de cosa que el IL deja sin ambigüedad.
  • callvirt 0x06000007 — llamar a Customer.get_Name() mediante un MethodDef, porque Customer está definido en este ensamblado, de modo que el getter de su propiedad es un método local, no una referencia.

Otras instrucciones que llevan tokens siguen la misma regla: ldfld/stfld llevan un token de Field (0x04…) o MemberRef, ldtoken lleva el asa que le hayas pedido capturar a typeof/fieldof, castclass y box llevan un token de tipo (0x01…, 0x02… o 0x1B…) y ldsfld sobre un estático externo lleva un MemberRef.

Resolver un token y saltar a su destino

Aquí es donde la vista IL se convierte en un navegador y no en un simple listado. Cuando Glass renderiza callvirt 0x0A000012, hace lo que hace el runtime: divide el token en el id de tabla 0x0A y la fila 0x12, busca la fila 18 de la tabla MemberRef y resuelve el padre, el nombre y la firma: StringBuilder.Append(string). En la interfaz el miembro resuelto es un hipervínculo. Haz clic y Glass navega hasta la definición de ese miembro (siguiendo el MemberRef hacia el ensamblado referenciado si está cargado, o mostrando la firma externa si no lo está). Un token MethodDef como 0x06000007 se resuelve dentro del ensamblado actual, así que al hacer clic saltas directamente a get_Name en el mismo árbol.

callvirt 0x0A000012 dividir el token de 4 bytes byte alto 0x0A MemberRef 3 bytes bajos 0x000012 fila 18 tabla MemberRef, fila 18 StringBuilder::Append (string) clic → saltar a la definición ldstr 0x70000001 token al heap #US → literal "Customer: " heaps #Strings #US #Blob #GUID

Los heaps: dónde viven las cadenas y los blobs

Las tablas no son toda la historia. Junto a ellas, los metadatos tienen cuatro heaps:

  • #Strings — los nombres de los identificadores (nombres de tipos, de métodos, espacios de nombres). Las filas de las tablas guardan offsets dentro de este heap, no el texto en sí.
  • #US (UserString) — los literales de cadena que usa tu código. El operando de un ldstr es un token 0x70… cuyos bytes bajos son un offset dentro de #US.
  • #Blob — blobs binarios: firmas, valores constantes, argumentos de atributos personalizados.
  • #GUID — el MVID del ensamblado y otros GUID.

Así que ldstr 0x70000001 se resuelve de forma distinta a un token de tabla: Glass lee el offset 0x000001 en el heap #US y saca el literal —"Customer: "— y lo muestra en línea. (Si quieres anclar esto en código, System.Reflection.Metadata lo modela directamente: MetadataReader.GetUserString(UserStringHandle) devuelve exactamente ese literal, y MetadataReader.GetBlobReader te da acceso crudo al heap #Blob. MetadataTokens.EntityHandle(int token) convierte un token crudo en un asa tipada, que es en esencia la división que realiza Glass.)

Leer la cabecera del cuerpo del método

Por encima de las instrucciones, todo cuerpo de método empieza con una cabecera, y la vista IL la refleja. Hay dos formatos:

  • Formato tiny. Se usa cuando el método no tiene locales, ni manejadores de excepciones, y una profundidad de pila de 8 o menos. Toda la cabecera es un solo byte: los dos bits bajos marcan “tiny” y los seis bits altos contienen el tamaño del código. No hay nada más que leer.
  • Formato fat. Se usa para todo lo demás. Una cabecera de 12 bytes lleva las flags, la profundidad de max stack, el tamaño del código y un token de firma de variables locales.

Ese token de firma de locales es en sí un token de metadatos a la tabla StandAloneSig (0x11…). No nombra un método ni un tipo: apunta a un blob que describe los locales del método. En nuestro ejemplo Describe, la línea .locals init (…StringBuilder V_0) y la línea .maxstack 2 salen ambas directamente de la cabecera fat; el tipo del único local se recuperó resolviendo el token StandAloneSig a su blob de firma. Cuando ves .maxstack y una lista .locals en la vista IL, estás leyendo una cabecera fat decodificada.

Lo que la vista IL muestra y el C# oculta

La razón para bajar al IL es que el C# es con pérdidas en la dirección de la reconstrucción. Algunas cosas que suele suavizar de forma rutinaria:

  • Boxing explícito. object o = 42; desaparece en una asignación en C#, pero el IL muestra box [System.Runtime]System.Int32 con un token de tipo. Si persigues una asignación de memoria o una InvalidCastException sorprendente, las instrucciones box/unbox.any solo son visibles aquí.
  • Estructura de ramas. Un foreach, un while, un switch con coincidencia de patrones y un ternario pueden reconstruirse a un C# de aspecto similar, pero el IL muestra las instrucciones exactas br, brtrue, blt y switch y sus offsets de destino. Cuando un bucle reconstruido parece mal, lee las ramas.
  • La sobrecarga exacta a la que se enlaza. En C#, Append(...) podría ser cualquiera de una docena de sobrecargas de StringBuilder.Append. En IL el token de call/callvirt se resuelve a una firma. Decodifícalo y sabrás con precisión qué sobrecarga eligió el compilador: la ambigüedad desaparece.

Esta es la recompensa práctica. Cuando la reconstrucción de C# de Glass es segura, confía en ella. Cuando es ambigua —genéricos que no hicieron un round-trip limpio, una build de release optimizada, un ensamblado al que le quitaron los nombres— cambia a IL, lee los tokens y haz clic para resolver lo que la vista de más alto nivel tuvo que adivinar. El IL más los metadatos es la verdad de base del ensamblado; la vista C# es la comodidad construida encima.

Hacia dónde ir después

Abre en Glass un ensamblado que sea tuyo, elige cualquier método con unas pocas llamadas externas y cambia a la vista IL. Decodifica un token de call a mano —divide el byte alto, encuentra la tabla, cuenta hasta la fila— y luego haz clic para confirmar que Glass te lleva al mismo miembro. Una vez que ese round-trip te resulte natural, la vista IL deja de intimidar y se convierte en la forma más rápida de responder “¿qué hace esto realmente?” cuando el C# no lo dice. Glass es gratuito; descárgalo desde la página del producto y lee tus binarios hasta el fondo.

Prueba Nebula.NET

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