文章分类

站点统计

  • 分类总数: 13 个
  • 文章总数: 145 篇
  • 评论总数: 47 条
  • 附件总数: 59 个
  • 建站日期: 2008-08-18
  • 访问总数: 472869 人次
  • RSS订阅: 文章|评论

Cecil学习笔记[1]

Admin 于 2008-09-23 03:50:41 发表.Net

订阅: http://www.kaiyuan8.org/Feed/Article_93.aspx
引用: 点这里获取地址 (UTF-8)
通过PostSharp4ViewState实现以AOP方式进行ViewState管理 < Cecil学习笔记[1] > 比较6个.NET控制反转(DI/IoC)框架

1.  Cecil学习

1.1.  Cecil简介

    要说Cecil则不能不提MONO,MONO是一个致力于在 Linux, Solaris, Mac OS X, Windows, and Unix等多个系统上运行.NET程序的平台,而Cecil则是 MONO下的一个子项目。简单地说,Cecil库用来修改符合ECMA CIL规范的.NET可执行文件。
    Cecil使用简单却功能强大,提供比.NET本身Reflection更多的操作功能,更重要的是它是开源的,于是深得许多大牛的喜爱。(数年前就有大牛推荐偶学这个库了,可惜当时没有实现。)MONO虽然没有提供Cecil的tut,但是Cecil库本身的代码是 self pxplanation的,所以学习起来并不困难,再加上有许多开源的工具,学起来更是方便。Cecil开源项目的网址是:http://groups.google.com/group/mono-cecil/web/projects-using-cecil。

1.2.  Cecil总体结构

    用Reflector载入Cecil.dll并查看其结构,可以发现Cecil主要由七个名称空间构成(有时在Reflector中查看比直接阅读源代码还轻松些,特别是对于这种整体结构的把握),如图1所示。


图1  Reflector中查看Cecil的整体结构

    分别浏览各名称空间,便可了解各空间中代码的主要功能。

1.2.1.  Mono.Cecil空间

主要包含一些:
(1)一些.NET关键概念(或者说元数据表)的定义,如AssemblyDefinition、ModuleDefinition、 MethodDefinition、EventDefinition等。这些定义的名称直接代表了相应的元数据概念,都是很好理解的,代表了这些概念的 MONO实现(或都说Cecil实现)。
(2)一些关键标志的定义,如AssemblyKind:
代码:

  1. public enum AssemblyKind 
  2.     Dll, 
  3.     Console, 
  4.     Windows 

(3)一个操作Assembly的总体方法定义,主要是AssemblyFactory类,其间提供了打开、保存Assembly的相关方法,代码示例如下:

  1. AssemblyDefinition assembly = AssemblyFactory.GetAssembly(assemblyFullPath); 
  2. //取得AssemblyDefinition后,可以获取程序集的相关信息,如程序集全名,如下 
  3. string fullName = assembly.Name.FullName; 

 

1.2.2.  Mono.Cecil.Binary空间

    从名字就可以看出来,该名称空间主要定义了二进制操作相关的代码和概念。这其中包括:
(1)基本PE结构及其操作,如DOSHeader、PEFileHeader、PEOptionalHeader,以及.NET对PE结构的扩展,如CLIHeader。同时还将一些PE中的概念扩展为类,比如RVA,更加易于编程。
(2)提供了资源和文件(Image)读写的相关类,如ImageReader、ResourceWriter等,但这几个类都是internal属性,一般用于被Cecil(当前程序集)中别的类调用,而不是由用户直接调用。
    利用Reflector的分析功能可以清楚得出哪些代码调用这几个读写类,如图2所示。
图2  利用分析功能查看读写类的调用情况

1.2.3.  Mono.Cecil.Cil空间

    这是非常关键,也是相对较复杂的一个空间,因为其中的大部分代码都和IL与方法的操作有关。
(1)首先是几个关键的enum。包括定义所有IL代码的Code:
代码:

  1. public enum Code 
  2.     Nop, 
  3.     Break, 
  4.     Ldarg_0, 
  5.     Ldarg_1, 
  6.     Ldarg_2, 
  7.     Ldarg_3, 
  8.     Ldloc_0, 
  9.     //略 

    其它几个enum均是与方法和IL代码流程相关的,比如ExceptionHandlerType、FlowControl(直接跳转?条件跳转?)、OpCodeType和OprandType等。
还有几个enum用于Cecil内部使用,比如MethodHeader和MethodDataSection,

(2)将一些关键的.NET概念定义为类,比如GuidAttribute、ExceptionHandler、OpCode,便于编程使用。比较重要的是Instruction类,该类将IL指令定义为类,提供了许多非常有用的属性,用以获取指令的相关信息:
代码:

  1. public sealed class Instruction : ICodeVisitable 
  2.     // Fields 
  3.     private Instruction m_next; 
  4.     private int m_offset; 
  5.     private OpCode m_opCode; 
  6.     private object m_operand; 
  7.     private Instruction m_previous; 
  8.     private SequencePoint m_sequencePoint; 
  9.  
  10.     // Methods 
  11.     internal Instruction(OpCode opCode); 
  12.     internal Instruction(OpCode opCode, object operand); 
  13.     internal Instruction(int offset, OpCode opCode); 
  14.     internal Instruction(int offset, OpCode opCode, object operand); 
  15.     public void Accept(ICodeVisitor visitor); 
  16.     public int GetSize(); 
  17.  
  18.     // Properties 
  19.     public Instruction Next { getset; } 
  20.     public int Offset { getset; } 
  21.     public OpCode OpCode { getset; } 
  22.     public object Operand { getset; } 
  23.     public Instruction Previous { getset; } 
  24.     public SequencePoint SequencePoint { getset; } 

(3)定义了几个功能强大的类,用于操作方法和IL。方法层次的为MethodBody类,IL层次的为CilWorker类。后者提供了几种对IL指令的最基本操作,如Append、InsertAfter、InsertBefore、Remove和Replace,而这五种操作又是建立在更底层的Create与Emit两大类方法上。因此,实践过程中,这两个类的使用应该是比较多的。
比如,下面的代码将一个现有的IL指令替换为nop:
代码:

  1. //代替换指令为ins,worker为CilWorker 
  2.     int size = ins.GetSize(); 
  3.     ins.OpCode = OpCodes.Nop; 
  4.     ins.Operand = null
  5.     for (int i = 1; i < size; i++) 
  6.     { 
  7.         Instruction instr = worker.Create(OpCodes.Nop); 
  8.         worker.InsertAfter(ins, instr); 
  9.     } 

 

1.2.4.  Mono.Cecil.Metadata空间

    看名称就可以知道,该空间主要与元数据的操作相关。其实对.NET文件或是方法、IL代码的任务操作,归根结底都是对底层的元数据进行操作。因此,其它空间中的许多方法最后会引用到Metadata空间中的有关代码。
    同样,还是让我们简单将该空间中的类型进行分类。
(1)将元数据的物理结构定义为类。其中包括元数据堆,如BlobHeap、GuidHeap、StringHeap、UserStringsHeap 等;此外是将所有支持的元数据表以Row和Table的形式进行定义,比如ModuleRow、ModuleTable等。
(2)将一些元数据底层的关键概念进行了定义,比如MetadataToken类,枚举包括CodedIndex、ElementType和TokenType。
    大多数情况下,该空间中的类和方法,编程时不会直接使用,用户通常只会用到别的名称空间中的代码,并在内部调用Metadata空间的代码。以保存修改后的程序集为例,我们所需编写的代码仅仅就是下面几行:

代码:

  1. CilWorker _worker; 
  2. ... 
  3. _worker.InsertBefore(target_instruction, some_instruction);  //这里对代码进行修改 
  4. ... 
  5. AssemblyFactory.SaveAssembly(asm_def, saveFileName); 

    所有的工作都由SaveAssembly方法在内部完成了。不妨用Reflector的分析功能看看从SaveAssembly方法到Metadata空间内部方法的调用过程。这次的分析,我们寻找一个中间点,既Cecil空间的ReflectorWriter类的 VisitMethodDefiniton方法,从该方法到SaveAssembly的调用过程如图3所示:
图3  SaveAssembly方法到VisitMethodDefinition的调用过程

    初步可以这样理解,SaveAssembly保存程序集时需要向文件中写入修改后的元数据,其中方法的元数据是通过VisitMethodDefinition实现的。下面是VisitMethodDefinition的代码:
代码:

  1. public override void VisitMethodDefinition(MethodDefinition method) 
  2.     MethodTable methodTable = this.m_tableWriter.GetMethodTable(); 
  3.     MethodRow row = this.m_rowWriter.CreateMethodRow(RVA.Zero, method.ImplAttributes, method.Attributes, this.m_mdWriter.AddString(method.Name), this.m_sigWriter.AddMethodDefSig(this.GetMethodDefSig(method)), this.m_paramIndex); 
  4.     methodTable.Rows.Add(row); 
  5.     this.m_methodStack.Add(method); 
  6.     method.MetadataToken = new MetadataToken(TokenType.Method, (uint) methodTable.Rows.Count); 
  7.     this.m_methodIndex++; 
  8.     if (RequiresParameterRow(method.ReturnType)) 
  9.     { 
  10.         this.InsertParameter(this.m_tableWriter.GetParamTable(), method.ReturnType.Parameter, 0); 
  11.     } 
  12.     this.VisitParameterDefinitionCollection(method.Parameters); 

    可以清楚地看到,上面的方法中有多处调用了元数据空间中的代码,比如CreateMethodRow创建了一个方法的Row,AddString用于在#Strings流中添加方法名称,并且为方法新建了一个MetadataToken。
    从这里看出,Cecil库的设计非常巧妙而复杂。幸运的是作为编程者无需和如此复杂的内部调用打交道,会使用SaveAssembly就足够了。

1.2.5.  Mono.Cecil.Signatures空间和Mono.Cecil.Text空间和Mono.Xml

    这三个空间就不多说了。Signatures全部都是关于签名的类,并且都是internal,是无法提供外部程序集直接使用的;Text提供了关于Unicode字符串的一些方法;最后的Xml是一些关于xml的内部操作定义。这三个空间的重要性和对我们的意义不大。

1.3.  Cecil的基本操作

1.3.1.  读取IL代码

    Cecil对IL代码的操作(包括读取、修改与保存)是Cecil是基本功能,也是入门最好示例。下面的代码部分参考http://www.rainsts.net。
    先看sample.exe的代码,后面将利用Cecil对此sample进行各种操作。代码很简单,没什么好解释的。
代码:

  1. using System; 
  2.  
  3. class program 
  4.   public static void Main() 
  5.   { 
  6.     Console.WriteLine("Cecil is fun"); 
  7.   } 

    在编写操作sample.exe的代码之前,先要熟悉一下Instruction类的成员,因为以后的诸多操作都是在此基础上进行的。Instruction类的定义如下:
代码:

  1. public sealed class Instruction : ICodeVisitable 
  2.    // Fields 
  3.    private Instruction m_next; 
  4.    private int m_offset; 
  5.    private Mono.Cecil.Cil.OpCode m_opCode; 
  6.    private object m_operand; 
  7.    private Instruction m_previous; 
  8.    private Mono.Cecil.Cil.SequencePoint m_sequencePoint; 
  9.  
  10.    // Methods 
  11.    internal Instruction(Mono.Cecil.Cil.OpCode opCode); 
  12.    internal Instruction(Mono.Cecil.Cil.OpCode opCode, object operand); 
  13.    internal Instruction(int offset, Mono.Cecil.Cil.OpCode opCode); 
  14.    internal Instruction(int offset, Mono.Cecil.Cil.OpCode opCode, object operand); 
  15.    public void Accept(ICodeVisitor visitor); 
  16.    public int GetSize(); 
  17.  
  18.    // Properties 
  19.    public Instruction Next { getset; } 
  20.    public int Offset { getset; } 
  21.    public Mono.Cecil.Cil.OpCode OpCode { getset; } 
  22.    public object Operand { getset; } 
  23.    public Instruction Previous { getset; } 
  24.    public Mono.Cecil.Cil.SequencePoint SequencePoint { getset; } 

      其中Offset代表该指令在本方法中的偏移(还记得ildasm反编译代码中的行号吗?),OpCode和Operand分别代码操作码和操作数,而Next和Previous则对反混淆非常有用,分别指向后一个和前一个指令。
    首先来看看如何利用Cecil读取并显示sample的所有方法和每个方法的IL代码。
代码:

  1. using System; 
  2. using Mono.Cecil; 
  3. using Mono.Cecil.Cil; 
  4.  
  5. class Program 
  6.   public static void Main() 
  7.   { 
  8.     AssemblyDefinition asm = AssemblyFactory.GetAssembly("sample.exe"); 
  9.     foreach (TypeDefinition tp in asm.MainModule.Types) 
  10.     { 
  11.       Console.WriteLine(tp.Name); 
  12.       foreach (MethodDefinition method in tp.Methods) 
  13.       { 
  14.         Console.WriteLine("  "+method.Name); 
  15.         Console.WriteLine("  .maxstack {0}", method.Body.MaxStack); 
  16.         foreach (Instruction ins in method.Body.Instructions) 
  17.         { 
  18.           Console.WriteLine("  L_{0}: {1} {2}", ins.Offset.ToString("x4"), 
  19.           ins.OpCode.Name, 
  20.           ins.Operand is String ? String.Format("\"{0}\"", ins.Operand) : ins.Operand); 
  21.         } 
  22.       } 
  23.     } 
  24.   } 

    编译时,只需要利用/reference:Mono.Cecil.dll选项添加对Cecil的引用即可。最终disp运行的结果如下:
I:\tmp>disp

program
  Main
  .maxstack 8
  L_0000: nop
  L_0001: ldstr "Cecil is fun"
  L_0006: call System.Void System.Console::WriteLine(System.String)
  L_000b: nop
  L_000c: ret

1.3.2.  修改IL代码

    下面在上面示例的基础上将难度提升一点点,主要是修改IL指令。最简单的,将ldstr的指令修改为别的字符串。这里,ldstr便是指令的 OpCode,而其后接的String便是Operand。要修改ldstr指令,需要定位到该指令所在的方法。一般方法是通过“程序集-->模块 -->类型-->方法”顺序循环查找的方法实现,而这里由于指令位于入口方法中,因此可以直接采用程序集的EntryPoint属性得到。
    下面在disp.cs基础上进行修改(这里只列出Main方法):
代码:

  1. public static void Main() 
  2.   { 
  3.     AssemblyDefinition asm = AssemblyFactory.GetAssembly("sample.exe"); 
  4.     MethodDefinition method=asm.EntryPoint; 
  5.     foreach(Instruction ins in method.Body.Instructions) 
  6.     { 
  7.       if(ins.OpCode.Name == "ldstr" && (string)ins.Operand == "Cecil is fun"
  8.       { 
  9.         Console.WriteLine("Find target instruction, start modify.."); 
  10.         ins.Operand="Cecil is SO fun"
  11.       } 
  12.     } 
  13.     AssemblyFactory.SaveAssembly(asm, "_sample.exe"); 
  14.     Console.WriteLine("Save complete"); 
  15.   } 

    编译并运行后,生成目标文件_sample.exe,用Reflector查看,目标指令已经被修改了。

1.3.3.  添加新的方法和IL代码

    下面我们来演示第三步:添加新的指令。这种添加指令的操作很像静态地代码注入。我们的目标是给sample示例添加最简单的字符串编码保护:既 Main方法中的ldstr指令不再直接读入“Cecil is fun”字符串,而是一段乱码(这里采用Base64编码),并新建一个方法用于运行时的解码。
    直接看代码吧,添加了注释,应该非常容易理解:
代码:

  1. using System; 
  2. using System.Text; 
  3.  
  4. using Mono.Cecil; 
  5. using Mono.Cecil.Cil; 
  6.  
  7. class Program 
  8.   public static void Main() 
  9.   { 
  10.     AssemblyDefinition asm = AssemblyFactory.GetAssembly("sample.exe"); 
  11.     MethodDefinition method=asm.EntryPoint; 
  12.     TypeDefinition tp=(TypeDefinition)method.DeclaringType; 
  13.     Instruction insertPoint=null
  14.     
  15.     //定义新方法 
  16.     MethodAttributes attr = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static;    
  17.     AssemblyNameReference asmNameRef = new AssemblyNameReference("mscorlib""",new Version("2.0.0.0")); 
  18.     TypeReference ret = new TypeReference("String","System",asmNameRef,false); 
  19.     MethodDefinition new_method=new MethodDefinition("StringDecode",attr,ret); 
  20.     
  21.     ParameterDefinition para=new ParameterDefinition(ret); 
  22.     new_method.Parameters.Add(para);    
  23.     tp.Methods.Add(new_method); 
  24.     
  25.     //给新方法定义代码 
  26.     new_method.Body.MaxStack=8; 
  27.     MethodReference mr; 
  28.     CilWorker worker=new_method.Body.CilWorker; 
  29.     
  30.     //插入call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8() 
  31.     mr= asm.MainModule.Import(typeof(Encoding).GetMethod("get_UTF8")); 
  32.     worker.Append(worker.Create(OpCodes.Call,mr)); 
  33.     
  34.     //插入ldarg.0 
  35.     worker.Append(worker.Create(OpCodes.Ldarg_0)); 
  36.     
  37.     //插入call uint8[] [mscorlib]System.Convert::FromBase64String(string) 
  38.     mr= asm.MainModule.Import(typeof(Convert).GetMethod("FromBase64String")); 
  39.     worker.Append(worker.Create(OpCodes.Call,mr)); 
  40.     
  41.     //插入callvirt instance string [mscorlib]System.Text.Encoding::GetString(uint8[]) 
  42.     mr=asm.MainModule.Import(typeof(Encoding).GetMethod("GetString",new Type[]{typeof(Byte[])}));    
  43.     worker.Append(worker.Create(OpCodes.Callvirt,mr)); 
  44.     worker.Append(worker.Create(OpCodes.Ret)); 
  45.     
  46.     
  47.     //修改老方法 
  48.     foreach(Instruction ins in method.Body.Instructions) 
  49.     { 
  50.       if(ins.OpCode.Name == "ldstr" && (string)ins.Operand == "Cecil is fun"
  51.       {        
  52.         Console.WriteLine("Find target instruction, start modify.."); 
  53.         ins.Operand="Q2VjaWwgaXMgZnVu"
  54.         insertPoint=ins; 
  55.         break
  56.       } 
  57.     } 
  58.     //插入对新方法的调用 
  59.     mr=asm.MainModule.Import(new_method);        
  60.     worker=method.Body.CilWorker; 
  61.     worker.InsertAfter(insertPoint,worker.Create(OpCodes.Call,mr));    
  62.     
  63.     //保存程序集 
  64.     Console.WriteLine("Save assembly..."); 
  65.     AssemblyFactory.SaveAssembly(asm,"_sample2.exe"); 
  66.     
  67.   } 

    代码中值得关注的就是新方法与新代码的插入,难点是如何插入call指令。多看一些示例,多调试几次就可以掌握了。编译并运行,程序会修改sample.exe并生成_sample2.exe。用Reflector查看_sample2,可以发现Main方法已经被修改:
代码:

  1. public static void Main() 
  2.     Console.WriteLine(StringDecode("Q2VjaWwgaXMgZnVu")); 

    同时,program类中多出了我们新定义的方法StringDecode,代码如下:
代码:

  1. public static string StringDecode(string text1) 
  2.     return Encoding.UTF8.GetString(Convert.FromBase64String(text1)); 

    到这里,Cecil的学习告一段落。短短数页文字,无法尽述Cecil强大的功能,更多细节留待我们在日后慢慢发掘。下面的文章会介绍另一个库FlowAnalysis的基础知识。

 点击下载源代码

被阅859次, 0投一票Mono Cecil
  • 看完了要说点啥么?
  • 昵称 (不填说不了话)
  • 信箱地址 (不会被公开,但是不填也说不了话)
  • 网址 (这个不填也成)
Powered by MiniBoke v2.0.0.8 Build 0828

Copyright © 2008 开源吧!. All rights reserved.

粤ICP备07500939号