Skip to content
← Todas las publicaciones
· Delta1 Labs DecompilationGlass

Decompilar enums [Flags]: cómo Glass.NET convierte ldc.i4.3 de nuevo en Read | Write

Una combinación de banderas como Read | Write se pliega a un único literal entero antes de llegar al IL, así que el | que escribiste desaparece. Este análisis lee el IL real del código de banderas, explica las señales de metadatos de FlagsAttribute y de las constantes con nombre que permiten a un decompilador descomponer un entero en una suma de miembros con nombre, y muestra cómo Glass.NET reconstruye los patrones | & ~ y HasFlag en lugar de dejarte (Permissions)3.

Un enum [Flags] es uno de los modismos más antiguos de .NET, y uno de los más engañosos de leer de vuelta desde un ensamblado compilado. Escribes algo expresivo — Permissions.Read | Permissions.Write — y esperas, con razón, que el decompilador te muestre exactamente eso. Pero para cuando tu código llega al IL, el | suele haber desaparecido, los dos nombres de miembro han desaparecido, y todo lo que sobrevive es un literal entero pelado. Que recuperes Read | Write o un plano (Permissions)3 depende por completo de lo bien que el decompilador lea un puñado de señales de metadatos. Este artículo lee el IL real y muestra cómo Glass.NET, el decompilador de .NET gratuito, reconstruye toda la familia de operaciones con banderas.

Qué es realmente un enum en los metadatos

Un enum no es una construcción especial del runtime. Es un tipo por valor que deriva de System.Enum, lleva un tipo entero subyacente (int por defecto) y lista sus miembros como campos static literal con nombre. Observa:

[Flags]
public enum Permissions
{
    None    = 0,
    Read    = 1,
    Write   = 2,
    Execute = 4,
    All     = Read | Write | Execute   // = 7
}

Apunta Glass al tipo y pide su forma:

glass decompile Acme.Storage.dll --type Acme.Storage.Permissions

Lo que Glass lee de los metadatos son cuatro cosas: el tipo deriva de System.Enum; tiene un campo de instancia con nombre especial value__ de tipo int (ahí se registra el tipo subyacente); tiene cinco campos static literal con nombre, cada uno con un valor constante en el heap #Blob; y lleva un atributo personalizado System.FlagsAttribute. Esas son las únicas entradas. No hay ningún “esto es Read-o-Write” almacenado en ningún sitio — la combinación All = 7 es simplemente otro campo constante. Todo lo demás es reconstrucción.

El OR que se desvanece

Ahora la parte que confunde a las herramientas ingenuas. Aquí hay un método que abre un archivo con dos banderas:

public Stream Open(string path) =>
    _fs.Open(path, Permissions.Read | Permissions.Write);

Pide a Glass el IL:

glass il Acme.Storage.dll --method Acme.Storage.FileService.Open

Las instrucciones relevantes son:

ldarg.0
ldfld     class IFileSystem Acme.Storage.FileService::_fs
ldarg.1
ldc.i4.3
callvirt  instance class Stream IFileSystem::Open(string, valuetype Permissions)

No hay ninguna instrucción or. Permissions.Read y Permissions.Write son ambos constantes en tiempo de compilación (1 y 2), así que el compilador de C# evaluó 1 | 2 durante la compilación y emitió un único ldc.i4.3. El OR a nivel de bits que escribiste lo ejecutó el compilador, no el programa. Este es el problema central: el operador de origen y los nombres de los miembros no están en el IL en absoluto. La única pista de que 3 es algo más que un entero corriente es la firma de Open, cuyo último parámetro está tipado como valuetype Permissions.

Ese contexto de tipo es el anzuelo. Glass ve una constante entera fluyendo hacia un parámetro (o variable local, o campo) de tipo enum, y cambia de “imprimir el número” a “descomponer el número contra los miembros de este enum”.

ldc.i4.3 un entero pelado en la pila metadatos del enum Permissions [Flags] underlying = int None = 0 Read = 1 (bit 0) Write = 2 (bit 1) Execute = 4 (bit 2) Read | Write 3 = 0b011 bits 0 y 1 activos Descomposición: 3 → bits activos {0,1} → {Read, Write} → residuo 0 FlagsAttribute es el permiso para hacerlo; el tipo del parámetro dice que el 3 es un Permissions no quedan bits residuales, así que no se emite ningún cast | (Permissions)x al final

La regla de descomposición, y dónde se vuelve sutil

Glass descompone un valor constante recorriendo los miembros definidos del enum y emparejando bits. La regla tiene que manejar tres casos que un enfoque de una sola línea tipo “OR de los miembros” resuelve mal.

Cero. El valor 0 se reconstruye como None si existe un miembro con valor 0, y en caso contrario como (Permissions)0. Nunca se reconstruye como un OR vacío.

Miembros combinados. El valor 7 podría mostrarse como Read | Write | Execute, pero el enum también define All = 7. Un buen decompilador prefiere el miembro que coincide exactamente: Glass emite Permissions.All, porque es casi con seguridad lo que el autor referenció, y es una forma legal y más legible del mismo valor. La descomposición solo recurre a unir bits individuales con OR cuando ningún miembro con nombre coincide con el valor completo.

Bits residuales. El valor 9 activa el bit 0 (Read) y el bit 3, que ningún miembro define. Glass emite Permissions.Read | (Permissions)8 — nombra cada bit que puede y hace un cast del sobrante, de modo que la reconstrucción sigue produciendo exactamente 9. Nunca descarta en silencio el 8 ni inventa un miembro para él.

Por eso [Flags] importa tanto para la salida. Sin el atributo, Glass no descompondrá en absoluto — trata el enum como un simple conjunto de valores discretos e imprime (Permissions)3, porque un enum que no es de banderas no ofrece ninguna promesa de que 3 sea una combinación significativa en lugar de un valor indefinido. El atributo es el único bit de metadatos que convierte un cast plano en código fuente legible con banderas.

Combinaciones, pruebas y limpiezas en tiempo de ejecución

Cuando los operandos no son ambos constantes, los operadores sobreviven hasta el IL y Glass los lee de vuelta directamente.

Un OR en tiempo de ejecución — current | extra donde ambos son locales — compila a una instrucción or real, y Glass reconstruye current | extra.

Una prueba de bandera escrita a mano:

if ((perm & Permissions.Write) != 0) { ... }

compila a ldarg, ldc.i4.2, and, y luego una bifurcación sobre el resultado. Glass ve el and contra una constante de un solo miembro seguido de una comparación con no-cero y reconstruye la prueba a nivel de bits exacta — no la reescribe como HasFlag.

Una prueba de bandera escrita con el ayudante:

if (perm.HasFlag(Permissions.Write)) { ... }

compila a un box Permissions del argumento y un callvirt a System.Enum::HasFlag. Ese punto de llamada es inconfundible, así que Glass reproduce perm.HasFlag(Permissions.Write) tal cual. Mantener distintas las dos formas importa: se comportan de forma idéntica para una sola bandera pero difieren con argumentos de banderas combinadas, y cuando auditas código desconocido quieres leer lo que el autor realmente escribió, no una reescritura normalizada.

Limpiar una bandera — perm & ~Permissions.Write — compila la constante ~2 a 0xFFFFFFFD plegada en tiempo de compilación, y luego un and. Aquí Glass reconoce la máscara de todos-los-bits-menos-uno contra el ancho del enum y reconstruye perm & ~Permissions.Write en lugar de perm & (Permissions)4294967293, que es correcto pero ilegible.

Verlo de principio a fin

Decompila el método completo y recuperas la forma legible:

glass decompile Acme.Storage.dll --method Acme.Storage.FileService.Configure
public void Configure(Permissions baseline)
{
    Permissions effective = baseline | Permissions.Read;   // or en tiempo de ejecución
    if (effective.HasFlag(Permissions.Execute))            // llamada a Enum.HasFlag
        effective &= ~Permissions.Write;                   // and con ~máscara plegada
    _current = effective;
}

Cada operador aquí se recuperó de una señal de IL distinta: el or de un opcode or real, el HasFlag de la llamada virtual con caja, la limpieza de un and contra un complemento plegado. Nada de esto se almacenó como código fuente. Ese es todo el trabajo de un decompilador — leer las señales que el compilador dejó atrás y reconstruir la intención encima de ellas.

Por qué vale la pena entender esto

Los enums de banderas aparecen por todas partes en los ensamblados reales: modos de acceso a archivos, binding flags, opciones de regex, ajustes de serialización, tus propios interruptores de características. Cuando haces trabajo legítimo — depurar una biblioteca cuyo código no tienes, auditar un paquete NuGet antes de confiar en él, aprender cómo un framework conecta sus opciones, o revisar fronteras de interop — un decompilador que aplana en silencio Read | Write a 3 te cuesta lo único que viniste a buscar: la intención del autor. Glass.NET es gratuito, así que puedes leer cualquier ensamblado cargado de banderas de esta forma sin una barrera de licencia, y como la reconstrucción se apoya en los metadatos y el IL reales, puedes confiar en que Read | Write realmente es 3 y en que un | (Permissions)8 al final significa que de verdad había un bit que nadie nombró.

Prueba Nebula.NET

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