The Member Tree
Assemblies and Modules
The root of every .NET assembly is represented by the
AssemblyDefinition
class. This class exposes basic information such as
name, version and public key token, but also a collection of all modules
that are defined in the assembly. Modules are represented by the
ModuleDefinition
class.
Below an example that enumerates all modules defined in an assembly.
var assembly = AssemblyDefinition.FromFile(...);
foreach (var module in assembly.Modules)
Console.WriteLine(module.Name);
Most .NET assemblies only have one module. This main module is also
known as the manifest module, and can be accessed directly through the
AssemblyDefinition.ManifestModule
property.
Executable modules can have an entry point, which can be obtained using
the ManagedEntryPoint
property:
var entryPoint = module.ManagedEntryPoint;
Often, modules also contain a static module constructor, which is executed the moment the module is loaded into memory by the CLR (and thus before the entry point is executed). AsmResolver provides helper methods to quickly locate such a constructor:
var cctor = module.GetModuleConstructor();
Types
Types form logical units or data type defined in a module, and are
represented by the TypeDefinition
class.
Inspecting Types in a Module
Types defined in a module are exposed through the ModuleDefinition.TopLevelTypes
property. A top level types is any non-nested type. Nested types
are exposed through the TypeDefinition.NestedTypes
.
Below is an example program that iterates through all types recursively and prints them:
public const int IndentationWidth = 3;
private static void Main(string[] args)
{
var module = ModuleDefinition.FromFile(...);
DumpTypes(module.TopLevelTypes);
}
private static void DumpTypes(IEnumerable<TypeDefinition> types, int indentationLevel = 0)
{
string indentation = new string(' ', indentationLevel * IndentationWidth);
foreach (var type in types)
{
// Print the name of the current type.
Console.WriteLine($"{indentation}- {type.Name} : {type.MetadataToken}");
// Dump any nested types.
DumpTypes(type.NestedTypes, indentationLevel + 1);
}
}
Alternatively, you can get all the types including nested types using the
ModuleDefinition.GetAllTypes()
method:
var module = ModuleDefinition.FromFile(...);
foreach (var type in module.GetAllTypes())
Console.WriteLine(type.FullName);
Creating New Types
New types can be created by calling one of its constructors:
ModuleDefinition module = ...
var newType = new TypeDefinition(
"Namespace",
"Name",
TypeAttributes.Public,
module.CorLibTypeFactory.Object);
Warning
For classes, ensure that you specify a non-null base type or the CLR will not load the binary properly.
For structures, make sure that your type inherits from System.ValueType
:
ModuleDefinition module = ...
var newType = new TypeDefinition(
"Namespace",
"Name",
TypeAttributes.Public,
module.CorLibTypeFactory.CorLibScope
.CreateTypeReference("System", "ValueType")
.ImportWith(module.DefaultImporter));
Interfaces in a .NET module do not have a base type, and as such, creating new interfaces will not require specifying one:
ModuleDefinition module = ...
var newType = new TypeDefinition(
"Namespace",
"IName",
TypeAttributes.Public | TypeAttributes.Interface);
Once a type has been constructed, it can be added to either a ModuleDefinition
as a top-level type, or to another TypeDefinition
as a nested type:
ModuleDefinition module = ...;
module.TopLevelTypes.Add(newType);
TypeDefinition type = ...;
type.NestedTypes.Add(newType);
Fields
Fields comprise all the data a type stores, and form the internal structure
of a class or value type. They are represented using the FieldDefinition
class.
Inspecting Fields in a Type
The TypeDefinition
class exposes a collection of fields that the type
defines:
foreach (var field in type.Fields)
Console.WriteLine($"{field.Name} : {field.MetadataToken}");
Fields have a signature which contains the field's type.
FieldDefinition field = ...
Console.WriteLine($"Field type: {field.Signature.FieldType}");
Fields can also have constants attached, exposed via the Constant
property:
FieldDefinition field = ...
if (field.Constant is { } constant)
Console.WriteLine($"Field Constant Data: {BitConverter.ToString(constant.Value>Data)}");
For fields that have an RVA attached (such as fields with an initial
value set), you can access the FieldRva
property containing the
ISegment
value with the raw data. This is in particular useful for
inspecting fields containing the initial raw data of an array.
FieldDefinition field = ...
if (field.FieldRva is { } rva)
Console.WriteLine($"Field Initial Data: {BitConverter.ToString(rva.WriteIntoArray())}");
Refer to Reading and Writing File Segments for more
information on how to use ISegment
s.
Creating New Fields
Creating and adding new fields can be done by using one of its constructors.
ModuleDefinition module = ...;
var field = new FieldDefinition(
"MyField",
FieldAttributes.Public,
module.CorLibTypeFactory.Int32);"
Fields can be added to a type:
TypeDefinition type = ...;
type.Fields.Add(field);
Most properties in FieldDefinition
are mutable, allowing you to configure
however you want your new field to be.
Methods
Methods are functions defined in a type, and provide a way to define
operations that can be applied to a type. They are represented using
the MethodDefinition
class.
Inspecting Methods in a Type
The TypeDefinition
class exposes a collection of methods that the type
defines:
foreach (var method in type.Methods)
Console.WriteLine($"{method.Name} : {method.MetadataToken}");
AsmResolver provides helper methods to find constructors in a type:
var parameterlessCtor = type.GetConstructor();
var parameterizedCtor = type.GetConstructor(module.CorLibFactory.Int32);
var cctor = type.GetStaticConstructor();
Methods and fields have a Signature
property, that contain the return
and parameter types:
MethodDefinition method = ...
Console.WriteLine($"Return type: {method.Signature.ReturnType}");
Console.WriteLine($"Parameter types: {string.Join(", ", method.Signature.ParameterTypes)}");
However, for reading parameters from a method definition, it is
preferred to use the Parameters
property instead of the
ParameterTypes
property stored in the signature. This is because the
Parameters
property automatically binds the types to the parameter
definitions that are associated to these parameter types. This provides
additional information, such as the name of the parameter:
foreach (var parameter in method.Parameters)
Console.WriteLine($"{parameter.Name} : {parameter.ParameterType}");
Methods may or may not be assigned a method body. This can be verified
using HasMethodBody
:
if (method.HasMethodBody)
{
// ...
}
Typically, a method body implemented using the Common Intermediate
Language (CIL), the bytecode used by .NET. This method body can be
inspected using the CilMethodBody
property:
if (method.CilMethodBody is { } body)
{
foreach (var instruction in body.Instructions)
Console.WriteLine(instruction);
}
For more information on CIL method bodies, refer to CIL Method Bodies.
Creating New Methods
Creating new methods can be done either through one of its constructors, taking a name, attributes, and a method signature.
For static methods, use the MethodSignature.CreateStatic
to create
the signature:
ModuleDefinition module = ...;
var method = new MethodDefinition(
"MyMethod",
MethodAttributes.Public | MethodAttributes.Static,
MethodSignature.CreateStatic(
module.CorLibTypeFactory.Void, // Return type
module.CorLibTypeFactory.Int32, // Parameter 1
module.CorLibTypeFactory.String // Parameter 2
));
Similarly, for instance methods, use the MethodSignature.CreateInstance
to create the signature:
ModuleDefinition module = ...;
var method = new MethodDefinition(
"MyMethod",
MethodAttributes.Public,
MethodSignature.CreateInstance(
module.CorLibTypeFactory.Void, // Return type
module.CorLibTypeFactory.Int32, // Parameter 1
module.CorLibTypeFactory.String // Parameter 2
));
AsmResolver provides helper methods to create special methods such as constructors that automatically set the right attributes and initialize it with a default method body.
ModuleDefinition module = ...;
var ctor = MethodDefinition.CreateConstructor(module.CorLibTypeFactory.Int32);
var cctor = MethodDefinition.CreateStaticConstructor();
After creating methods, they can be added to a type:
TypeDefinition type = ...;
type.Methods.Add(method);
Most properties in MethodDefinition
are mutable, allowing you to configure
however you want your new method to be.
Properties and Events
Properties and Events add special semantics to groups of methods, and are
represented using the PropertyDefinition
and EventDefinition
classes
respectively.
Obtaining properties and events is similar to obtaining methods and
fields; TypeDefinition
exposes them in a list as well:
foreach (var property in type.Properties)
Console.WriteLine($"{property.Name} : {property.MetadataToken}");
foreach (var @event in type.Events)
Console.WriteLine($"{@event.Name} : {@event.MetadataToken}");
Properties and events have methods associated to them. These are
accessible through the Semantics
property:
foreach (MethodSemantics semantic in property.Semantics)
Console.WriteLine($"{semantic.Attributes} {semantic.Method.Name} : {semantic.MetadataToken}");
For properties, there are helpers defined to quickly access the getter or setter methods of the property:
MethodDefinition getter = property.GetMethod;
MethodDefinition setter = property.SetMethod;
Similarly, for events, there exists helpers for obtaining the add, remove, and fire methods:
MethodDefinition adder = property.AddMethod;
MethodDefinition remover = property.RemoveMethod;
MethodDefinition fire = property.FireMethod;