Mono Cecil

下面的两张图来自 Cecil 网站,通过它我们大概知道 Cecil 类型结构,看上去和 Reflection 类似。

  1. 反编译

  获取程序集方法的 IL 代码。

  代码原形:

public class Class1
{
  public void Test()
  {
    Console.WriteLine("Hello, World!");
  }
}

  Lutz Roeder's Reflector 反编译结果:

.method public hidebysig instance void Test() cil managed
{
  .maxstack 8
  L_0000: nop 
  L_0001: ldstr "Hello, World!"
  L_0006: call void [mscorlib]System.Console::WriteLine(string)
  L_000b: nop 
  L_000c: ret 
}

  使用 Mono Cecil 进行反编译:

namespace ConsoleApp
{
    using System;
    using System.Text;
    using Mono.Cecil;
    using Mono.Cecil.Cil;
    class Program
    {
        static void Main(string[] args)
        {
            AssemblyDefinition asm = AssemblyDefinition.ReadAssembly("MyLibrary.dll");
            foreach (TypeDefinition type in asm.MainModule.Types)
            {
                if (type.Name.Equals("Class1"))
                {
                    foreach (MethodDefinition method in type.Methods)
                    {
                        Console.WriteLine("Method Name: "+method.Name);
                        Console.WriteLine(".maxstack {0}", method.Body.MaxStackSize);
                        foreach (Instruction ins in method.Body.Instructions)
                        {
                            Console.WriteLine("L_{0}: {1} {2}", ins.Offset.ToString("X4"),
                                ins.OpCode.Name, ins.Operand is string ? string.Format("\"{0}\"", ins.Operand) : ins.Operand);
                        }//end for Instruction
                    }//end for MethodDefinition
                }
            }//end for TypeDefinition

            Console.ReadLine();
        }
    }
}

  输出:



  2. 修改 IL 代码

  继续以上面的代码为例,我们的目标是将 "Hello, World!" 改成 "Hello, C#!"。当然,更复杂也可以,只不过需要你自己花时间了。

  AssemblyDefinition asm = AssemblyDefinition.ReadAssembly("MyLibrary.dll");
            foreach (TypeDefinition type in asm.MainModule.Types)
            {
                if (type.Name.Equals("Class1"))
                {
                    foreach (MethodDefinition method in type.Methods)
                    {
                        if (method.Name == "Test")
                        {
                            foreach (Instruction ins in method.Body.Instructions)
                            {
                                if (ins.OpCode.Name.Equals("ldstr") && ins.Operand.ToString().StartsWith("Hello"))
                                {
                                    ins.Operand = "Hello,C#";
                                }
                            }
                        }

                    }//end for MethodDefinition
                }
            }//end for TypeDefinition
            asm.Write("MyLibrary.dll");

  用 Lutz Roeder's Reflector 打开 Test.dll 看看结果。

.method public hidebysig instance void Test() cil managed
{
  .maxstack 8
  L_0000: nop 
  L_0001: ldstr "Hello, C#!"
  L_0006: call void [mscorlib]System.Console::WriteLine(string)
  L_000b: nop 
  L_000c: ret 
}

  达成所愿~~~~~

  3. 代码注入

  完成代码修改以后,我们玩一个更难点的,注入额外的代码。(好像有点做病毒的意思,呵呵~)

  任务目标是在 Console.WriteLine("Hello, World!"); 前注入 Console.WriteLine("Virus? No!");,还好这不是真的破坏性代码。

AssemblyDefinition asm = AssemblyFactory.GetAssembly("MyLibrary.dll");
foreach (TypeDefinition type in asm.MainModule.Types)
{
  if (type.Name == "Class1")
  {
    foreach (MethodDefinition method in type.Methods)
    {
      if (method.Name == "Test")
      {
        foreach (Instruction ins in method.Body.Instructions)
        {
          if (ins.OpCode.Name == "ldstr" && (string)ins.Operand == "Hello, World!")
          {
            CilWorker worker = method.Body.CilWorker;
            
            Instruction insStr = worker.Create(OpCodes.Ldstr, "Virus? NO!");
            worker.InsertBefore(ins, insStr);
            MethodReference refernce = asm.MainModule.Import(typeof(Console).GetMethod("WriteLine", 
              new Type[]{typeof(string)}));
            Instruction insCall = worker.Create(OpCodes.Call, refernce);
            worker.InsertAfter(insStr, insCall);
            Instruction insNop = worker.Create(OpCodes.Nop);
            worker.InsertAfter(insCall, insNop);
            break;
          }
        }
      }
    }
  }
}

  继续用 Lutz Roeder's Reflector 看一下结果。

  C#

public void Test()
{
  Console.WriteLine("Virus? NO!");
  Console.WriteLine("Hello, World!");
}

  IL

.method public hidebysig instance void Test() cil managed
{
  .maxstack 8
  L_0000: nop 
  L_0001: ldstr "Virus? NO!"
  L_0006: call void [mscorlib]System.Console::WriteLine(string)
  L_000b: nop 
  L_000c: ldstr "Hello, World!"
  L_0011: call void [mscorlib]System.Console::WriteLine(string)
  L_0016: nop 
  L_0017: ret 
}

  用反射测试一下修改后的程序集。

AssemblyFactory.SaveAssembly(asm, "Test.dll");
Assembly testAssembly = Assembly.LoadFrom("Test.dll");
Type class1Type = testAssembly.GetType("MyLibrary.Class1");
Object o = Activator.CreateInstance(class1Type);
class1Type.InvokeMember("Test", BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod, null, o, null);

  输出:

Virus? No!
Hello, World!

  一切OK! 注入更复杂的代码,甚至是为类型添加方法,或者直接用来做 AOP…… 好像还有很多事可做……

  4. 获取程序集信息

AssemblyDefinition asm = AssemblyFactory.GetAssembly("Learn.Library.dll");
Console.WriteLine("Kind:{0}", asm.Kind);
Console.WriteLine("Runtime:{0}", asm.Runtime);

  输出:

Kind:Dll
Runtime:NET_2_0

  利用 ModuleDefinition.Image 属性,我们可以获取程序集几乎全部的细节信息。包括 CLIHeader、DebugHeader、DOSHeader、FileInformation、HintNameTable、 ImportAddressTable、ImportLookupTable、ImportTable、MetadataRoot、 PEFileHeader、PEOptionalHeader、ResourceDirectoryRoot、Sections、TextSection 等。

  ------------

  继续关注和研究,也欢迎大家一起交流。

有一个名为 MyLibrary.dll 的程序集,内有 Class1 类型,我们需要动态创建其对象,并调用 Test 方法。注入的要求是在执行 Test 内部代码前先执行一个外部程序集的方法 InjectMethod。

 class Class1
{
   void Test()
  {
    Console.WriteLine("Hello, World!");
  }
}

  注入代码

public class Program
{
  public static void InjectMethod()
  {
    Console.WriteLine("Haha...");
  }
  static void Main(string[] args)
  {
    AssemblyDefinition asm = AssemblyFactory.GetAssembly("MyLibrary.dll");
    foreach (TypeDefinition type in asm.MainModule.Types)
    {
      if (type.Name == "Class1")
      {
        foreach (MethodDefinition method in type.Methods)
        {
          if (method.Name == "Test")
          {
            Instruction ins = method.Body.Instructions[0];
            CilWorker worker = method.Body.CilWorker;
            MethodInfo m = typeof(Program).GetMethod("InjectMethod", BindingFlags.Static | BindingFlags.Public);
            MethodReference refernce = asm.MainModule.Import(m);
            Instruction insCall = worker.Create(OpCodes.Call, refernce);
            worker.InsertBefore(ins, insCall);
            Instruction insNop = worker.Create(OpCodes.Nop);
            worker.InsertAfter(insCall, insNop);
          }
        }
      }
    }
    Assembly testAssembly = AssemblyFactory.CreateReflectionAssembly(asm); 
    Type class1Type = testAssembly.GetType("MyLibrary.Class1");
    Object o = Activator.CreateInstance(class1Type);
    class1Type.InvokeMember("Test", BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod, null, o, null);
  }
}

  输出:

Haha...
Hello, World!

  动态注入和静态注入不同,它不会修改目标程序集文件,因此不会有强类型签名等问题,也不易发生相关 "版权" 问题。可用来跳过某段验证代码,或者替换某些内部执行代码,用途有点像 "外挂"。使用动态注入实现 AOP 可能比较有意思,我们可以遍历添加了 "[AOP(aopMethodName=)]" 方法特性的所有类型,然后动态注入 AOP 拦截代码。

  -----------------------

  除了 Mono.Cecil,还有另外一个选择 PostSharp。

  http://www.postsharp.org

  About PostSharp

  With PostSharp, you can develop custom attributes that change your code. And you can do more!

  PostSharp is a post-compiler: an open platform for analysis and transformations of .net assemblies after compilation.

  ASPect-Oriented Programming (AOP) or Policy Injection are two of the major applications, but only two of them.

  PostSharp Laos is a high-level aspect weaver that makes aspect-oriented programming (AOP) extremely easy. But Laos is only an illustration of the complete Platform. PostSharp is used to perform low-level MSIL injection. It serves as a base for persistence layers, optimizers or custom AOP weavers.

上午和同事讨论实现 AOP 时,曾聊到一个问题,如何自动执行初始化操作,从而实现使用 MSIL Injection 或者 Emit 进行方法拦截。由于目标类型可能存放于一个类库中,除非强制要求调用人员执行一个初始化代码,否则初始化注入就只能是摆设。网上常见的动态代理方式,要求用户必须使用代理工厂来创建目标类型,而我们聊到的方案中并不打算这么做。

  今天在博客园看到一篇文章 《程序集的初始化及合并》给了我点启发。当用户调用类库的时候,必然会先载入 "<Module>" 类型,那么其静态构造必然被执行,因此在 "<Module>" 静态构造中静态注入一些代码,即可实现全局的初始化操作。本文无意讨论 AOP 实现,重点是如何实现类库自动初始化操作。和原文作者使用 ILMerge 不同的是,我将使用 Mono Cecil 创建 "<Module>" 的静态构造方法。

using Mono.Cecil;
using Mono.Cecil.Cil;
...
AssemblyDefinition asm = AssemblyFactory.GetAssembly("MyLibrary.dll");
foreach (TypeDefinition type in asm.MainModule.Types)
{
  if (type.Name == "<Module>")
  {
    MethodAttributes attr = MethodAttributes.Private | MethodAttributes.HideBySig | 
      MethodAttributes.SpecialName |   MethodAttributes.RTSpecialName | MethodAttributes.Static;
    // 创建静态构造方法
    AssemblyNameReference asmNameRef = new AssemblyNameReference("mscorlib", "", 
      new Version("2.0.0.0"));
    TypeReference ret = new TypeReference("Void", "System", asmNameRef, false);
    
    MethodDefinition cctor = new MethodDefinition(".cctor", attr, ret);
    type.Constructors.Add(cctor);
    // 实现静态构造方法体
    cctor.Body.MaxStack = 8;
    CilWorker worker = cctor.Body.CilWorker;
    worker.Append(worker.Create(OpCodes.Nop));
    worker.Append(worker.Create(OpCodes.Ldstr, "moudle init..."));
    worker.Append(worker.Create(OpCodes.Call, 
      asm.MainModule.Import(typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }))));
    worker.Append(worker.Create(OpCodes.Nop));
    worker.Append(worker.Create(OpCodes.Ret));
    
    break;
  }
}
// 保存修改后的程序集
AssemblyFactory.SaveAssembly(asm, "MyLibrary2.dll");
// 随便类库中某个类型,查看初始化是否执行。
System.Reflection.Assembly exeasm = AssemblyFactory.CreateReflectionAssembly(asm);
Type exetype = exeasm.GetType("MyLibrary.Class1");
object o = Activator.CreateInstance(exetype);

  输出:

moudle init...
Class1 static...
Class1 Constructor...

  使用 Lutz Roeder's Reflector 查看 MyLibrary2.dll 的 IL 代码。

.class private auto ansi <Module>
{
  .method private hidebysig specialname rtspecialname static void .cctor() cil managed
  {
    .maxstack 8
    L_0000: nop 
    L_0001: ldstr "moudle init..."
    L_0006: call void [mscorlib]System.Console::WriteLine(string)
    L_000b: nop 
    L_000c: ret 
  }
}

  本文同时也演示了如何使用 Mono Cecil 动态扩充程序集类型的方法。可以想象,利用 Mono Cecil + MSBuild Task 能做的事太多了,好的坏的,都行。

1. 添加程序集引用

AssemblyDefinition asm = AssemblyFactory.GetAssembly("MyLibrary.dll");
// 应该是程序集完整信息,包括名称、语言以及版本号。
AssemblyNameReference cecil = new AssemblyNameReference("Mono.Cecil", "", new Version("0.5.0.0"));
asm.MainModule.AssemblyReferences.Add(cecil);
foreach (AssemblyNameReference r in asm.MainModule.AssemblyReferences)
{
  Console.WriteLine(r.FullName);
} 
AssemblyFactory.SaveAssembly(asm, "MyLibrary2.dll");

  2. 添加类型

  在程序集中创建新的类型及其成员。

AssemblyDefinition asm = AssemblyFactory.GetAssembly("MyLibrary.dll");
AssemblyNameReference asmRef = new AssemblyNameReference("mscorlib", "", new Version("2.0.0.0"));
TypeReference baseType = new TypeReference("Object", "System", asmRef, false);
TypeReference returnType = new TypeReference("Void", "System", asmRef, false);
TypeReference intType = new TypeReference("Int32", "System", asmRef, false);
// 创建类型
TypeAttributes myClassAttribute = TypeAttributes.Class | TypeAttributes.Public;
TypeDefinition myClass = new TypeDefinition("Class1", "MyNamespace", myClassAttribute, baseType);
asm.MainModule.Types.Add(myClass);
// 添加默认构造方法
MethodAttributes ctorAttribute = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName;
MethodDefinition ctor = new MethodDefinition(".ctor", ctorAttribute, returnType);
myClass.Constructors.Add(ctor);
// 创建默认构造方法体
ctor.Body.MaxStack = 8;
CilWorker worker = ctor.Body.CilWorker;
worker.Append(worker.Create(OpCodes.Ldarg_0));
worker.Append(worker.Create(OpCodes.Call, asm.MainModule.Import(typeof(Object).GetConstructor(new Type[0]))));
worker.Append(worker.Create(OpCodes.Nop));
worker.Append(worker.Create(OpCodes.Ret));
// 添加字段
FieldDefinition field = new FieldDefinition("i", intType, FieldAttributes.Private);
myClass.Fields.Add(field);
// ...
AssemblyFactory.SaveAssembly(asm, "MyLibrary2.dll");

  还可以使用 PropertyDefinition、EventDefinition、MethodDefinition 添加更多成员,其使用方法比较简单,和我以前写 Emit 文章比较类似,本文不再详述。


你可能感兴趣的:(AOP,String,Module,assembly,Class,cil)