项目地址:Mono.Cecil
项目描述:In simple English, with Cecil, you can load existing managed assemblies, browse all the contained types, modify them on the fly and save back to the disk the modified assembly.
类似项目:Microsoft CCI
Common Compiler Infrastructure: Metadata API
Common Compiler Infrastructure: Code Model and AST API
Common Compiler Infrastructure: Sample applications
Common Compiler Infrastructure - Contrib
对比评价:来自StackOverflow
Mono.Cecil has better, more understandable and easy in use object model. However, I had an ugly bug when used it in my program (reference to the wrong method was saved in the assembly; I think there was some bug with metadata tokens handling)
Microsoft.CCI has an ugly, utterly over-designed object model in the same time lacking many simple features; however, it‘s more mature than Mono.Cecil. Finally, I abandoned Mono.Cecil and used Microsoft.CCI for my program.
基本示例
Cecil是对已编译生成IL的程序集进行操作,所以先写一个简单的Console exe程序,这里项目名称使用Cecil.Program:
using System; using System.Reflection; namespace Cecil.Program { class Program { static void Main(string[] args) { TestType tt = new TestType(); tt.SayHello(); tt.AboutMe(); Console.ReadKey(); } } public class TestType { [Obsolete] public void SayHello() { Console.WriteLine("\tHello Cecil !"); } public void AboutMe() { Type type = typeof(TestType); MethodInfo method = type.GetMethod("SayHello"); if (method.IsVirtual) Console.WriteLine("\tI‘m a virtual method"); else Console.WriteLine("\tI‘m a non-virtual method"); object[] attributes = method.GetCustomAttributes(false); if (attributes != null && attributes.Length > 0) { Console.WriteLine("\tI have the following attributes:"); foreach (object attr in attributes) Console.WriteLine("\t\t" + attr.GetType().Name); } } } }
这个程序集的运行结果如下:
方法SayHello的IL代码如下:
接下来使用另外一个Console exe程序来修改Cecil.Program.exe,项目名称使用Cecil:
using Mono.Cecil; using Mono.Cecil.Cil; AssemblyDefinition assembly = AssemblyFactory.GetAssembly("Cecil.Program.exe"); TypeDefinition type = assembly.MainModule.Types["Cecil.Program.TestType"]; MethodDefinition sayHello = null; foreach (MethodDefinition md in type.Methods) if (md.Name == "SayHello") sayHello = md; //Console.WriteLine(string value)方法 MethodInfo writeLine = typeof(Console).GetMethod("WriteLine" , new Type[] { typeof(string) }); //Console.WriteLine方法导入MainModule,并返回在AssemblyDefinition中的引用方式 MethodReference writeLineRef = assembly.MainModule.Import(writeLine); //在SayHello方法开始位置插入一条trace语句 // Console.WriteLine(">>Intercepting "); //如果插入的语句需要使用函数入参,则必须插入在OpCodes.Ldarg等指令之后 CilWorker worker = sayHello.Body.CilWorker; Instruction ldstr = worker.Create(OpCodes.Ldstr, ">>Intercepting " + sayHello.Name); Instruction call = worker.Create(OpCodes.Call, writeLineRef); Instruction first = sayHello.Body.Instructions[0]; worker.InsertBefore(first, call); worker.InsertBefore(call, ldstr); //在SayHello方法结束位置插入一条trace语句 // Console.WriteLine(">>Intercepted "); //语句必须插入在OpCodes.Ret指令的前面 int offset = sayHello.Body.Instructions.Count - 1; Instruction last = sayHello.Body.Instructions[offset--]; while (last.OpCode == OpCodes.Nop || last.OpCode == OpCodes.Ret) last = sayHello.Body.Instructions[offset--]; ldstr = worker.Create(OpCodes.Ldstr, ">>Intercepted " + sayHello.Name); worker.InsertAfter(last, ldstr); worker.InsertAfter(ldstr, call); //把SayHello方法改为虚方法 sayHello.IsVirtual = true; //给SayHello方法添加一个SerializableAttribute CustomAttribute attribute = new CustomAttribute( assembly.MainModule.Import( typeof(SerializableAttribute).GetConstructor(Type.EmptyTypes) )); sayHello.CustomAttributes.Add(attribute); AssemblyFactory.SaveAssembly(assembly, "Cecil.Program.modified.exe"); Console.WriteLine("Assembly modified successfully!"); Console.ReadKey();
编译生成Cecil.exe,然后把Cecil.Program.exe拷贝到这个目录下,运行Cecil.exe,便会在当前目录生成Cecil.Program.modified.exe,运行Cecil.Program.modified.exe结果如下:
修改后的方法SayHello的IL代码如下:
从上面的基本使用方法可以看出,Cecil的确是易于使用,对象模型结构非常实用,这里是官方网站的一个主要对象结构图:
IL指令的复杂性
在assembly、type、method级别上对程序集做修改是非常简单的,但是如果要修改方法体的IL代码,则可能会遇到一些较麻烦的事情,需要细致的处理
例如上面的SayHello方法如果是这样:
public void SayHello(bool print) { if (print) Console.WriteLine("\tHello Cecil !"); }
测试代码这样来调用:
TestType2 tt2 = new TestType2(); tt2.SayHello(true); tt2.SayHello(false); Console.ReadKey();
其运行结果只会输出一条Hello Cecil !消息,仍然使用Cecil.exe来修改这个程序集,其运行结果如下图:
调用tt2.SayHello(false);时,应该也会有一个>>Intercepted SayHello消息,但是没有输出,对比一下IL代码就清楚了:
修改后的IL代码如下:
IL_000b那一句,为false时就直接跳转到IL_0021这个返回指令上了,不会输出Intercepted的消息
使用Mono.Cecil也可以修改这个跳转地址,例如:
//得到指令brfalse.s Instruction jmp = sayHello.Body.Instructions[1]; .... //把跳转的目标地址改成IL_0017 ldstr指令位置 jmp.Operand = ldstr;