運用Mono.Cecil 反射讀取.NET程序集元數據


CLR自帶的反射機智和API可以很輕松的讀取.NET程序集信息,但是不能對程序集進行修改。CLR提供的是只讀的API,但是開源項目Mono.Cecil不僅僅可以讀取.NET程序集的元數據,還可以進行修改。

1 讀取程序集的版本信息

CLR定義的對象模型是以程序集為編譯和部署單元。一個程序集下面可以有多個模塊,模塊下面再包含類型,類型中定義方法,屬性和事件等。直接傳入一個完整的字符串表示的程序集,MONO可以解析它的基本信息:

var name = AssemblyNameReference.Parse ("Foo, version=2.0.0.0, culture=fr-FR");
Assert.AreEqual ("Foo", name.Name);
Assert.AreEqual (2, name.Version.Major);
Assert.AreEqual (0, name.Version.Minor);
Assert.AreEqual ("fr-FR", name.Culture);
 
 

 

你可以用.NET Reflector載入程序集,拷貝紅色方框所表示的內容,它是一個程序集的完整名稱

image

 

2  創建程序集

BLC自帶的API不能新建一個程序集文件,必須用源代碼,借助編譯器來生成.NET平台的MSIL程序集文件。請看下面的代碼

var module = ModuleDefinition.CreateModule ("Test.dll", ModuleKind.Dll);
Assert.AreEqual ("Test", module.Assembly.Name.Name);

module = ModuleDefinition.CreateModule ("Test.exe", ModuleKind.Console);
Assert.AreEqual ("Test", module.Assembly.Name.Name);
module.Write (file);

一句代碼創建程序集,傳入需要的屬性,另一句代碼保存創建的程序集對象。API背后完成了復雜的功能,把這二行代碼生成的程序集用.NET Reflector載入,可以正常打開,僅僅是沒有類型和方法定義。

 

3  讀取Attribute特性

請先看下面的代碼,比BCL自帶的反射讀取特性的代碼要省略很多,幾乎就是要什么,直接一個屬性就可以獲取到

ModuleDefinition module=......
var hamster = module.GetType ("Hamster");
Assert.IsTrue (hamster.HasCustomAttributes);
Assert.AreEqual (1, hamster.CustomAttributes.Count);
var argument = attribute.ConstructorArguments [0];
 
 

4 讀取類型成員的元數據

依然保持簡潔的API,所需要的值都可以直接從屬性中獲取,下面的代碼演示讀取方法的元數據:

ModuleDefinition module=......
var type = module.Types [1];
Assert.AreEqual ("Foo", type.Name);
Assert.AreEqual (2, type.Methods.Count);
var method = type.GetMethod ("Bar");
Assert.AreEqual ("Bar", method.Name);
Assert.IsTrue (method.IsAbstract);
Assert.IsNotNull (method.ReturnType);
Assert.AreEqual (1, method.Parameters.Count);
var parameter = method.Parameters [0];
Assert.AreEqual ("a", parameter.Name);
Assert.AreEqual ("System.Int32", parameter.ParameterType.FullName);
 
 

下面的代碼演示讀取字段的元數據:

var type = module.Types [1];
Assert.AreEqual ("Foo", type.Name);
Assert.AreEqual (1, type.Fields.Count);

var field = type.Fields [0];
Assert.AreEqual ("bar", field.Name);
Assert.AreEqual (1, field.MetadataToken.RID);
Assert.IsNotNull (field.FieldType);
Assert.AreEqual ("Bar", field.FieldType.FullName);
Assert.AreEqual (TokenType.Field, field.MetadataToken.TokenType);
Assert.IsFalse (field.HasConstant);
Assert.IsNull (field.Constant);
 
 

5 創建方法定義,對現有的類型或方法進行代碼注入或修改的起點

顯然,這個用途是Mono.Cecil流行起來的主要原因之一,可以進行代碼注入(Code Injection):

先來看,如何創建一個方法體

var object_ref = new TypeReference ("System", "Object", null, null, false);
var method = new MethodDefinition ("foo", MethodAttributes.Static, object_ref);
var body = new MethodBody (method);

var il = body.GetILProcessor ();

var first = il.Create (OpCodes.Nop);
var second = il.Create (OpCodes.Nop);
var third = il.Create (OpCodes.Nop);

body.Instructions.Add (first);
body.Instructions.Add (third);

body.Instructions.Insert (1, second);
 

把這段代碼翻譯成C#語言的方法定義,它應該是這樣的:

static object  foo ()
{
    
}

三個空指令,在C#中相當於空語句,對應的MSIL語句是NOP指令。

把這里延伸一下,我獲取到現有類型的方法引用,調用方法把它的MSIL指令全部清空,再插入我需要的指令,做到代碼注入:

body.Instructions.Clear();
body.Instructions.Add (myInstruction);
//or insert     
body.Instructions.Insert (2, myInstruction);
 

在我的前一篇文章中,講解如何應用這個技巧,做一個字符串混淆程序的模型,參考下面的地址
字符串混淆技術應用 設計一個字符串混淆程序 可混淆.NET程序集中的字符串

 

6  類型推斷

我們在寫C#程序時,當使用usiing指令后,可以不用全名稱來表示一個類型,比如下面的代碼

Console.WriteLine()
或是
System.Console.WriteLine()
 

從.NET Reflector中加載程序集,MSIL代碼中看到的類型或是方法調用,都是以全名稱表示的。編譯器在編譯時,自動幫我們將命名空間和類型連接到一起,編譯成MSIL代碼中。

 
image 
 

所以,參考下面的代碼,

Assert.AreEqual ("System.String System.String::Empty", string_empty.FullName);
var definition = string_empty.Resolve ();
Assert.AreEqual ("System.String System.String::Empty", definition.FullName);
 
var definition = string_length.Resolve ();
Assert.AreEqual ("get_Length", definition.Name);
Assert.AreEqual ("System.String", definition.DeclaringType.FullName);

var definition = list_add.Resolve ();
Assert.AreEqual ("System.Void System.Collections.Generic.List`1::Add(T)", definition.FullName);

var definition = try_get_value.Resolve ();
Assert.AreEqual ("System.Boolean System.Collections.Generic.Dictionary`2::TryGetValue(TKey,TValue&)", definition.FullName);
 

當有返型參數時,它的名字看起來有點怪怪的。舊的版本的.NET Reflector在反編譯成BCL類型時,沒有處理到這一點,導致反編譯后的源代碼不能編譯,新版本已經解決這個問題。.NET Reflector自從商業化之后,功能精進不休,配合.NET平台的進步,實為.NET開發中的必備工具之一。

 

.NET Reflector 8.1.0.35  : .NET Reflector

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM