Decompilar tipos locales de archivo: recuperar el modificador file a partir de un nombre de metadatos mutilado
El modificador file de C# 11 no tiene atributo ni marca. El compilador lo codifica mutilando el nombre del tipo en una forma impronunciable <File>F0__Type y emitiendo el tipo como interno de ensamblado. Un decompilador que imprima ese nombre tal cual produce código que no compila. Así es como Glass.NET lee la mutilación de vuelta a file class.
C# 11 añadió el modificador de acceso file: un tipo declarado file class Parser solo es visible dentro del único archivo de código fuente que lo declara. Dos archivos en el mismo proyecto —o dos archivos generados por código que cada uno emite un ayudante— pueden ambos declarar un file class Parser sin colisionar, y ninguno puede tocarse desde ningún otro sitio. Existe sobre todo para beneficio de los generadores de código, que emiten tipos de soporte dentro de la compilación del usuario y no deben filtrar nombres que choquen con el propio código del usuario ni con el de otro generador.
Desde el punto de vista de un decompilador, file es un problema interesante porque, como varias características recientes de C#, no deja ningún atributo tras de sí. No hay marca [FileLocal], ni modificador personalizado, ni bandera en la fila TypeDef que diga «este tipo se declaró file». El compilador codifica el modificador en el único lugar que sobrevive hasta los metadatos y está garantizado como único: el nombre del tipo. Mutila el nombre declarado en una forma que deliberadamente no es un identificador legal de C#, y baja la visibilidad a interno de ensamblado. Un decompilador que lea la tabla TypeDef e imprima lo que encuentra emitirá un tipo llamado <Validators>F0__Parser con accesibilidad internal —que no es ni lo que el autor escribió ni nada que el compilador de C# vaya a aceptar. Reconstruir el original significa reconocer la mutilación, revertirla y volver a colocar el tipo recuperado en un contenedor de archivo.
Esta entrada recorre el viaje de ida y vuelta completo: el código fuente, los metadatos que emite el compilador y el C# que Glass.NET reconstruye.
Lo que escribió el autor
Un tipo file normalmente mantiene un pequeño ayudante privado para un solo archivo. Aquí Validators.cs tiene una regla local de archivo más una fachada pública que la usa:
// Validators.cs
file sealed class PhoneRule
{
public static bool IsValid(string s) => s.Length == 10;
}
public static class Contact
{
public static bool Check(string phone) => PhoneRule.IsValid(phone);
}
PhoneRule es de ámbito file, así que nada fuera de Validators.cs puede nombrarlo —ni otro archivo del mismo ensamblado, ni un consumidor que referencie la DLL. Contact, declarado de forma normal, lo llama sin problema porque vive en el mismo archivo.
Lo que emite el compilador
A PhoneRule le pasan dos cosas en el camino hacia los metadatos. Primero, su nombre se mutila. El compilador construye un nombre de metadatos a partir de un identificador de archivo (derivado del nombre del archivo de código fuente), un ordinal que desambigua archivos que resulten compartir nombre, y el nombre del tipo declarado, envuelto de modo que el resultado no pueda escribirse en código fuente:
.class private sealed auto ansi beforefieldinit
'<Validators>F0__PhoneRule'
extends [System.Runtime]System.Object
{
.method public hidebysig static
bool IsValid (string s) cil managed
{
ldarg.0
callvirt instance int32 [System.Runtime]System.String::get_Length()
ldc.i4.s 10
ceq
ret
}
}
Fíjate en lo que no está: ningún CompilerGeneratedAttribute, ningún atributo personalizado de ningún tipo que anuncie el modificador. El nombre <Validators>F0__PhoneRule es el registro entero de la palabra clave file. Los < y > son la pista —están reservados precisamente para que el código de usuario nunca pueda producir un identificador que colisione con uno sintetizado por el compilador.
Segundo, la accesibilidad del tipo baja a interno de ensamblado. Un tipo de nivel superior solo puede ser public o interno en el sistema de tipos de la CLI —no hay visibilidad más fina a nivel de metadatos— así que el compilador emite PhoneRule como interno y se apoya en el nombre impronunciable para imponer la regla más fuerte de «un solo archivo» en tiempo de compilación. En ILDasm un tipo interno de nivel superior se lee como .class private, por eso la fila de arriba dice private aunque nunca escribieras private.
El sitio de llamada dentro de Contact.Check referencia el nombre mutilado directamente, porque para cuando se emite el IL el único nombre que tiene PhoneRule es el mutilado:
.method public hidebysig static bool Check (string phone) cil managed
{
ldarg.0
call bool '<Validators>F0__PhoneRule'::IsValid(string)
ret
}
Cada referencia a un tipo local de archivo dentro de su propio archivo —tipos base, tipos de campo, llamadas a métodos, typeof— lleva este token mutilado. Y como las reglas de C# prohíben que un tipo local de archivo aparezca en la firma de un miembro no-file, el token mutilado está garantizado a aparecer solo dentro de los tipos que vinieron del mismo archivo de código fuente. Ese límite importa para la reconstrucción: Glass nunca tiene que perseguir una referencia mutilada a través de una frontera de archivo.
La decompilación ingenua, y por qué falla
Un decompilador que recorra la tabla TypeDef y renderice cada fila literalmente produce esto:
internal sealed class <Validators>F0__PhoneRule // no compila
{
public static bool IsValid(string s) => s.Length == 10;
}
Eso está mal por dos motivos. El nombre <Validators>F0__PhoneRule no es un identificador legal, así que el archivo ni siquiera se analiza. Y internal es la accesibilidad de metadatos, no el modificador de código fuente que el autor eligió —reemitir internal descarta la intención file por completo y, peor aún, haría el tipo visible a todo el ensamblado si de algún modo pudieras renombrarlo, cambiando el significado del programa. Un decompilador fiel tiene que deshacer ambas transformaciones juntas.
Cómo lo lee de vuelta Glass.NET
Glass trata el nombre mutilado como un token estructurado en lugar de una cadena opaca. Su reconstrucción de un nombre de tipo ejecuta un pequeño decodificador:
- Un tipo de nivel superior cuyo nombre coincide con
<file>Fordinal__name es un tipo local de archivo. Glass lo divide en el identificador de archivo (Validators), el ordinal (0) y el nombre declarado (PhoneRule). - El nombre declarado se restaura como el identificador de código fuente del tipo, de modo que
PhoneRuleaparece exactamente como se escribió. - El modificador se reconstruye como
file, no elinternalque reportan los metadatos —la forma mutilada es evidencia más fuerte de la intención del autor que la accesibilidad de nivel de ensamblado, que el compilador se vio obligado a usar solo porque no existe una más estrecha en la capa de metadatos. - Cada referencia mutilada a este tipo en otras partes del ensamblado —la
call '<Validators>F0__PhoneRule'::IsValiddentro deContact, cualquier uso de campo o tipo base— se reescribe alPhoneRulerestaurado.
El identificador de archivo es más que decoración. Cuando Glass vuelve a disponer el código fuente, usa el contenedor <Validators> para agrupar cada tipo que lo comparte, de modo que los tipos locales de archivo aterrizan juntos en un archivo recuperado que refleja su agrupación original —y, de forma crítica, dos archivos de código fuente distintos que cada uno declaró un file class Parser (nombres mutilados distintos, <Validators>F0__Parser y <Rules>F0__Parser) se reconstruyen en archivos separados, que es la única disposición que compila. Juntarlos en un solo archivo reintroduciría el choque de nombres que el modificador file se inventó para prevenir.
El código fuente reconstruido hace el viaje de ida y vuelta limpiamente:
// Glass agrupa este tipo en un archivo recuperado identificado por su file identifier
file sealed class PhoneRule
{
public static bool IsValid(string s)
{
return s.Length == 10;
}
}
public static class Contact
{
public static bool Check(string phone)
{
return PhoneRule.IsValid(phone);
}
}
El modificador file está de vuelta, el nombre mutilado desapareció tanto de la declaración como del sitio de llamada, y la fachada pública Contact se lee exactamente como lo hacía en el código fuente.
Hacerlo desde la CLI
Apunta Glass al ensamblado y deja que reconstruya todo, agrupación de archivos incluida:
# Reconstruir a un árbol de código fuente; los tipos locales de archivo se reagrupan por file identifier
glass decompile Validators.dll -o ./recovered
# Inspeccionar un solo tipo, nombre mutilado y todo
glass decompile Validators.dll --type PhoneRule
Cuando quieras ver los metadatos en bruto con los que trabaja el decodificador —la fila .class private con el nombre <Validators>F0__PhoneRule y el token call mutilado— baja a la vista de IL:
glass il Validators.dll --type "<Validators>F0__PhoneRule"
Ver el nombre mutilado junto al file class recuperado es la forma más clara de entender lo que el modificador cuesta realmente en la capa de metadatos: nada más que una convención de nombres y una accesibilidad rebajada, reconstruidas leyendo la convención de vuelta.
Por qué importa esto
Los tipos locales de archivo aparecen con más frecuencia en código generado, que es exactamente el código que tienes más probabilidad de estar leyendo en un decompilador —tienes una DLL, un generador de código se ejecutó durante su compilación, y quieres entender lo que produjo. Si tu herramienta imprime internal class <Thing>F0__Helper, no aprendes casi nada y no puedes recompilar lo que ves. Recuperar el file class Helper real, agrupado tal como el generador lo emitió, convierte un volcado mutilado ilegible de vuelta en el código que realmente se escribió —que es el sentido entero de decompilar tu propio ensamblado o el de un tercero para entenderlo, depurarlo o interoperar con él.
Glass.NET es un decompilador de .NET gratuito con una interfaz Avalonia nativa de Visual Studio y una interfaz CLI/MCP, construido para reconstruir exactamente estas construcciones modernas de C# —las que viven en convenciones de metadatos en lugar de en atributos— para que lo que leas de vuelta sea el código fuente, no la contabilidad del compilador.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.