📜 ⬆️ ⬇️

Injection MSIL code into a third-party assembly using Mono.Cecil. Implementing AOP principles in NET

Introduction


In this article, I will discuss how you can add your code to existing .NET assemblies and how it relates to aspect-oriented programming. The article will be accompanied by working examples, as I believe that the code is the best way to convey the idea.

Many .NET developers know that you can use Reflection to access objects of someone else’s assembly. Using the types from System.Reflection, we can access many .NET objects of the assembly, view their metadata, and even use those objects that are restricted (for example, private methods of another class). But using Reflection has its limitations and the main reason for this is that the data you are working with through Reflection is still considered code. Thus, for example, you can get a CodeAccessSecurity exception if the assembly to which you are trying to apply Reflection prohibits it. For the same reason, Reflection is rather slow. But the most important thing for this article is that standard Reflection does not allow changing existing assemblies, only generate and save new ones.

Mono.Cecil


A qualitatively different approach is offered by the free open source library Mono.Cecil. The main difference between the Mono.Cecil approach and the Reflection approach is that this library works with the .NET assembly as a stream of bytes. When loading the assembly, Mono.Cecil parses the PE header, CLR header, MSIL code of classes and methods, etc. working directly with the stream of bytes representing the assembly. Thus, with the help of this library, you can arbitrarily (within the limits of the stipulated) modify the existing assembly.

Download Mono.Cecil here .
')
Immediately, I note that modifying a third-party assembly signed with a strong name will result in a reset of the signature, with all the ensuing consequences. After modification, the assembly can be re-signed (with the same key, if you have it, or with another - if, for example, you need to put the assembly in the GAC).

Small example


Immediately consider an example of using the capabilities of Mono.Cecil. Suppose we have a third-party build of a console application without source code, which has the type Program. We do not have access to the source code, but we want this application to display each message when calling each method, some message to the console. To do this, we write our own console application. As an argument at startup, we will pass the path to the target application:

using Mono.Cecil; using Mono.Cecil.Cil; class Program { static void Main(string[] args) { if (args.Length == 0) return; string assemblyPath = args[0]; //     Mono.Cecil var assembly = AssemblyDefinition.ReadAssembly(assemblyPath); //   Console.WriteLine,    Reflection var writeLineMethod = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }); //    ,  Reflection,    Mono.Cecil var writeLineRef = assembly.MainModule.Import(writeLineMethod); foreach (var typeDef in assembly.MainModule.Types) { foreach (var method in typeDef.Methods) { //       //     "Inject!" method.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ldstr, "Inject!")); //   Console.WriteLine,      -     "Injected". method.Body.Instructions.Insert(1, Instruction.Create(OpCodes.Call, writeLineRef)); } } assembly.Write(assemblyPath); } } 

When passing the path to a third-party assembly to our console application, it will add an IL code to the beginning of each method that displays the message “Inject!” To the console, and then saves the modified assembly. When you run the modified assembly, each method will write in the console "Inject!".

Let us dwell on the above code. NET is known to support many programming languages. This is achieved due to the fact that any code in any programming language is compiled into CIL - Common Intermediate Language - intermediate language. Why intermediate? Because after, the CIL code is converted to instructions of the corresponding processor. Thus, the code in any languages ​​is compiled into approximately the same CIL code, due to which you can use, for example, an assembly on VB in your C # project.

Thus, each assembly, conventionally speaking, is a set of metadata (which, for example, Reflection uses), and a set of instructions in CIL.

I will not elaborate on the description of CIL, since this is not the topic of this article. I will confine myself only to what will be important for further, namely, some features of the CIL instructions. Information about the presentation of metadata and other instructions you can always find on the Internet.

To begin, consider the part of the code from the example above:
 method.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ldstr, "Inject!")); method.Body.Instructions.Insert(1, Instruction.Create(OpCodes.Call, writeLineRef)); 

In this code, we got access to a set of CIL instructions of some method and add our own. The CIL instruction set can be found here:. When working directly with CIL, the stack is important. We can put some data on the stack and retrieve it from there (according to how the stack works). In the example above, using the Ldstr instruction, we put the string “Inject!” On the stack. Next, we call System.Console.WriteLine. Any method call calls the stack to get the necessary arguments. In this case, System.Console.WriteLine, we need an argument of type string, which we loaded onto the stack. The call instruction loads the arguments from the end, so you need to load the argument values ​​onto the stack in the usual way. Thus, this instruction will transfer control to the System.Console.WriteLine method with the string parameter equal to “Inject!”. This instruction set is equivalent to:
 System.Console.WriteLine("Injected!"); 


Since Mono.Cecil perceives the assembly as a set of instructions (bytes), we can change its contents without any restrictions. After adding the CIL code, we save it (as a set of bytes) and get the modified assembly.

Real application of code generation for implementation of an aspect-oriented approach



Consider applying the above approach to your own builds. Very often we want to execute some code when entering or exiting a method, and to have access to some data describing the method or its context. The simplest example is logger. If we want to record in the log entry and exit of each method, then writing uniform code at the beginning and end of each method can be very tedious. Also, in my opinion, this somewhat pollutes the code. In addition, we cannot access the method parameters on the stack automatically, and if we want to also record the state of the parameters at the input, we will have to do it manually. The second known problem is the implementation of INotifyPropertyChanged, which has to be assigned to each property manually.

Consider a different approach. To test it, create a new console application. Add a class:
 [AttributeUsage(AttributeTargets.Method)] public class MethodInterceptionAttribute : Attribute { public virtual void OnEnter(System.Reflection.MethodBase method, Dictionary<string, object> parameters) { } public virtual void OnExit() { } } 

The user can inherit from this class, override the OnEnter method, and apply the inherited attribute to any method. Our goal is to realize the following possibility: when entering a method marked with an attribute of type MethodInterceptionAttribute, call OnEnter, where to send a reference to the method and parameter set of this method in the form of <parameter_name: value>.

For experiments we will create two console applications. The first will contain the attribute definition and all the methods needed to inject code into third-party applications. The second application will be a test. We first consider the short code of the test application:

 class Program { static void Main(string[] args) { MethodToChange("Test"); } [TestMethodInterception()] public static void MethodToChange(string text) { Console.ReadLine(); } } public class TestMethodInterceptionAttribute : MethodInterceptionAttribute { public override void OnEnter(System.Reflection.MethodBase method, Dictionary<string, object> parameters) { Console.WriteLine("Entering method " + method.Name + "..." + Environment.NewLine); foreach (string paramName in parameters.Keys) { Console.WriteLine("Parameter " + paramName + " has value " + parameters[paramName] + Environment.NewLine); } } } 

This is a simple console application that calls the MethodToChange method with the text parameter equal to Test. This method is marked with the TestMethodInterceptionAttribute attribute inherited from MethodInterceptionAttribute. OnEnter is overridden to display information about any methods marked with this attribute to the console. Without pre-processing, this application will launch Console.ReadLine when it is started and exit.

Let's continue consideration of the main application (also console). To demonstrate the example MSIL code and to assist in further development, we will write the following auxiliary method:

 static void DumpAssembly(string path, string methodName) { System.IO.File.AppendAllText("dump.txt", "Dump started... " + Environment.NewLine); var assembly = AssemblyDefinition.ReadAssembly(path); foreach (var typeDef in assembly.MainModule.Types) { foreach (var method in typeDef.Methods) { if (String.IsNullOrEmpty(methodName) || method.Name == methodName) { System.IO.File.AppendAllText("dump.txt", "Method: " + method.ToString()); System.IO.File.AppendAllText("dump.txt", Environment.NewLine); foreach (var instruction in method.Body.Instructions) { System.IO.File.AppendAllText("dump.txt", instruction.ToString() + Environment.NewLine); } } } } } 

This method reads the existing MSIL code from any assembly method (or all) and writes it to the dump.txt file. How can this be useful? Suppose we know what specific code we want to add to a third-party build, but we don’t want to write all MSIL code from scratch. Then we will write this code in C # to some of our methods and make it a dump. After that, it will be much easier to write MSIL using Mono.Cecil, already having a ready-made example of how it will look (of course, other, more convenient methods can be used to view the MSIL code of assemblies).

Consider what we want to get at the beginning of each method (in the form of C #):

 var currentMethod = System.Reflection.MethodBase.GetCurrentMethod(); var attribute = (MethodInterceptionAttribute)Attribute.GetCustomAttribute(currentMethod, typeof(MethodInterceptionAttribute)); Dictionary<string, object> parameters = new Dictionary<string, object>(); //              parameters,    #   attribute.OnEnter(currentMethod, parameters); 

Part of the dump of this code on MSIL:

IL_0000: nop
IL_0001: call System.Reflection.MethodBase System.Reflection.MethodBase::GetCurrentMethod()
IL_0006: ldtoken EmitExperiments.MethodInterceptionAttribute
IL_000b: call System.Type System.Type::GetTypeFromHandle(System.RuntimeTypeHandle)
IL_0010: call System.Attribute System.Attribute::GetCustomAttribute(System.Reflection.MemberInfo,System.Type)
IL_0015: castclass EmitExperiments.MethodInterceptionAttribute
IL_001a: stloc V_1
IL_001e: ldloc V_1
IL_0022: callvirt System.Void EmitExperiments.MethodInterceptionAttribute::OnEnter()
...

Next, I simply provide the complete code for the InjectToAssembly method (with detailed comments), which will add the necessary code to all methods with the MethodInterceptionAttribute of the specified assembly:

 static void InjectToAssembly(string path) { var assembly = AssemblyDefinition.ReadAssembly(path); //   GetCurrentMethod() var getCurrentMethodRef = assembly.MainModule.Import(typeof(System.Reflection.MethodBase).GetMethod("GetCurrentMethod")); //   Attribute.GetCustomAttribute() var getCustomAttributeRef = assembly.MainModule.Import(typeof(System.Attribute).GetMethod("GetCustomAttribute", new Type[] { typeof(System.Reflection.MethodInfo), typeof(Type) })); //   Type.GetTypeFromHandle() -  typeof() var getTypeFromHandleRef = assembly.MainModule.Import(typeof(Type).GetMethod("GetTypeFromHandle")); //    MethodBase var methodBaseRef = assembly.MainModule.Import(typeof(System.Reflection.MethodBase)); //    MethodInterceptionAttribute var interceptionAttributeRef = assembly.MainModule.Import(typeof(MethodInterceptionAttribute)); //   MethodInterceptionAttribute.OnEnter var interceptionAttributeOnEnter = assembly.MainModule.Import(typeof(MethodInterceptionAttribute).GetMethod("OnEnter")); //    Dictionary<string,object> var dictionaryType = Type.GetType("System.Collections.Generic.Dictionary`2[System.String,System.Object]"); var dictStringObjectRef = assembly.MainModule.Import(dictionaryType); var dictConstructorRef = assembly.MainModule.Import(dictionaryType.GetConstructor(Type.EmptyTypes)); var dictMethodAddRef = assembly.MainModule.Import(dictionaryType.GetMethod("Add")); foreach (var typeDef in assembly.MainModule.Types) { foreach (var method in typeDef.Methods.Where(m => m.CustomAttributes.Where( attr => attr.AttributeType.Resolve().BaseType.Name == "MethodInterceptionAttribute").FirstOrDefault() != null)) { var ilProc = method.Body.GetILProcessor(); //   InitLocals  true,       false (      ) //      -  IL   . method.Body.InitLocals = true; //      attribute, currentMethod  parameters var attributeVariable = new VariableDefinition(interceptionAttributeRef); var currentMethodVar = new VariableDefinition(methodBaseRef); var parametersVariable = new VariableDefinition(dictStringObjectRef); ilProc.Body.Variables.Add(attributeVariable); ilProc.Body.Variables.Add(currentMethodVar); ilProc.Body.Variables.Add(parametersVariable); Instruction firstInstruction = ilProc.Body.Instructions[0]; ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Nop)); //    ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Call, getCurrentMethodRef)); //       currentMethodVar ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Stloc, currentMethodVar)); //        ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, currentMethodVar)); //     MethodInterceptionAttribute ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldtoken, interceptionAttributeRef)); //  GetTypeFromHandle (   typeof()) -  typeof(MethodInterceptionAttribute) ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Call, getTypeFromHandleRef)); //          MethodInterceptionAttribute.  Attribute.GetCustomAttribute ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Call, getCustomAttributeRef)); //     MethodInterceptionAttribute ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Castclass, interceptionAttributeRef)); //     attributeVariable ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Stloc, attributeVariable)); //   Dictionary<stirng, object> ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Newobj, dictConstructorRef)); //   parametersVariable ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Stloc, parametersVariable)); foreach (var argument in method.Parameters) { //    //     Dictionary<string,object> ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, parametersVariable)); //    ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldstr, argument.Name)); //    ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldarg, argument)); //  Dictionary.Add(string key, object value) ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Call, dictMethodAddRef)); } //     ,       OnEnter ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, attributeVariable)); ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, currentMethodVar)); ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, parametersVariable)); //  OnEnter.     ,    OnEnter    ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Callvirt, interceptionAttributeOnEnter)); } } assembly.Write(path); } } 

Let's not forget also about the Main method of our console application:

 static void Main(string[] args) { if (args.Length > 0) { string mode = args[0]; string path = args[1]; if (mode == "-dump") { string methodName = args.Length > 2 ? args[2] : String.Empty; DumpAssembly(path, methodName); } else if (mode == "-inject") { InjectToAssembly(args[1]); } } } 

Done! Now, if you run the main application with the -inject parameter, giving it the path to our test application, the code of the MethodToChange method changes as follows (obtained using Reflector):

 [TestMethodInterception] public static void MethodToChange(string text) { MethodBase currentMethod = MethodBase.GetCurrentMethod(); MethodInterceptionAttribute customAttribute = (MethodInterceptionAttribute) Attribute.GetCustomAttribute(currentMethod, typeof(MethodInterceptionAttribute)); Dictionary<string, object> parameters = new Dictionary<string, object>(); parameters.Add("text", text); customAttribute.OnEnter(currentMethod, parameters); Console.ReadLine(); } 


As required. Now, every method marked with TestMethodInterception will be intercepted and each call will be processed without writing a lot of duplicate code. To automate the process, you can use Post-Build events in VS, so that after the successful construction of a project, you can automatically process the finished assembly and implement code based on attributes. You can also create class or assembly level attributes to embed code at once into all methods of a class or assembly.

This approach is an example of using the techniques of aspect-oriented programming in .NET. I will not dwell in detail on what AOP is, in general terms you can always read about it on wikipedia . The most famous library that allows you to use the principles of AOP in .NET is PostSharp , which inspired me to study the possibilities of code injection into my assemblies to implement this functionality and, accordingly, writing this article.

Using AOP allows you to write clean and easily maintained code, mainly due to the fact that most of the code is generated automatically based on aspects.

In this article, I tried to describe in detail how code can be added to existing NET assemblies using Mono.Cecil, as well as how it can be used to benefit from the implementation of AOP principles in Net, hopefully, this will seem interesting or useful.

Source: https://habr.com/ru/post/108947/


All Articles