程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> [你必須知道的.NET]第二十五回:認識元數據和IL(中)

[你必須知道的.NET]第二十五回:認識元數據和IL(中)

編輯:關於.NET

書接上回[第二十四回:認識元數據和IL(上)],我們對PE文件、程序集、托管模塊,這些概念與元數據、IL的關系進行了必要的鋪墊,同時順便熟悉了以ILDASM工具進行反編譯的基本方法認知,下面是時候來了解什麼是元數據,什麼是IL這個話題了,我們繼續。

很早就有說說Metadata(元數據)和IL(中間語言)的想法了,一直在這篇開始才算腳踏實地的對這兩個階級兄弟投去些細關懷,雖然來得沒有《第一回:恩怨情仇:is和as》那麼迅速,但是Metadata和IL卻是絕對重量級的內容,值得我們在任何時間關注,本文就是開始。

3 元數據是什麼?

元數據,就是描述數據的數據。這一概念並非CLR之獨創,Metadata存在於任何對數據和數據關系中,例如程序集清單信息也被稱為程序集元數據。而不同系統的元數據也相應具有本身的特點,.NET元數據也是如此。那麼,CLR元數據描述的是哪些內容呢?正如前文的描述一樣,編譯之後,類型信息將以元數據的形式保存在PE格式文件中。.NET是基於面向對象的,所以元數據描述的主要目標就是面向對象的基本元素:類、類型、屬性、方法、字段、參數、特性等,主要包括:

定義表,描述了源代碼中定義的類型和成員信息,主要包括:TypeDef、MehodDef、FieldDef、ModuleDef、PropertyDef等。

引用表,描述了源代碼中引用的類型和成員信息,引用元素可以是同一程序集的其他模塊,也可以是不同程序集的模塊,主要包括:AssemblyRef、TypeRef、ModuleRef、MethodsRef等。

指針表,使用指針表引用未知代碼,主要包括:MethodPtr、FieldPtr、ParamPtr等。

堆,以stream的形式保存的信息堆,主要包括:#String、#Blob、#US、#GUIDe等。

如前文所述,我們以ILDasm.exe可以通過反編譯的方式,通過執行Ctrl+M快捷鍵來獲取該程序集所使用的MetaData信息列表,在.NET中每個模塊包含了44個CLR元數據表,如下:

表記錄 元數據表 說明 0(0) ModuleDef 描述當前模塊 1(0x1) TypeRef 描述引用Type,為每個引用到類型保存一條記錄 2(0x2) TypeDef 描述Type定義,每個Type將在TypeDef表中保存一條記錄 3(0x3) FieldPtr 描述字段指針,定義類的字段時的中間查找表 4(0x4) FieldDef 描述字段定義 5(0x5) MethodPtr 描述方法指針,定義類的方法時的中間查找表 6(0x6) MethodDef 描述方法定義 7(0x7) ParamPtr 描述參數指針,定義類的參數時的中間查找表 8(0x8) ParamDef 描述方法的參數定義 9(0x9) InterfaceImpl 描述有哪些類型實現了哪些接口 10(0xa) MemberRef 描述引用成員的情況,引用成員可以是方法、字段還有屬性。 11(0xb) Constant 描述了參數、字段和屬性的常數值 12(0xc) CustomAttribute 描述了特性的定義 13(0xd) FieldMarshal 描述了與非托管代碼交互時,參數和字段的傳遞方式。 14(0xe) DeclSecurity 描述了對於類、方法和程序集的安全性 15(0xf) ClassLayout 描述類加載時的布局信息 16(0x10) FieldLayout 描述單個字段的偏移或序號 17(0x11) StandAloneSig 描述未被任何其他表引用的簽名 18(0x12) EventMap 描述類的事件列表 19(0x13) EventPtr 描述了事件指針,定義事件時的中間查找表 20(0x14) Event 描述事件 21(0x15) PropertyMap 描述類的屬性列表 22(0x16) PropertyPtr 描述了屬性指針,定義類的屬性時的中間查找表 23(0x17) Property 描述屬性 24(0x18) MethodSemantics 描述事件、屬性與方法的關聯 25(0x19) MethodImpl 描述方法的實現 26(0x1a) ModuleRef 描述外部模塊的引用 27(0x1b) TypeSpec 描述了對TypeDef或者TypeRef的說明 28(0x1c) ImplMap 描述了程序集使用的所有非托管代碼的方法 29(0x1d) FieldRVA 字段表的擴展,RVA給出了一個字段的原始值位置 30(0x1e) ENCLog 描述在Edit-And-Continue模式中哪些元數據被修改過 31(0x1f) ENCMap 描述在Edit-And-Continue模式中的映射 32(0x20) Assembly 描述程序集定義 33(0x21) AssemblyProcessor 未使用 34(0x22) AssemblyOS 未使用 35(0x23) AssemblyRef 描述引用的程序集 36(0x24) AssemblyRefProcessor 未使用 37(0x25) AssemblyRefOS 未使用 38(0x26) File 描述外部文件 39(0x27) ExportedType 描述在同一程序集但不同模塊,有哪些類型 40(0x28) ManifestResource 描述資源信息 41(0x29) NestedClass 描述嵌套類型定義 42(0x2a) GenericParam 描述了泛型類型定義或者泛型方法定義所使用的泛型參數 43(0x2b) MethodSpec 描述泛型方法的實例化 44(0x2c) GenericParamConstraint 描述了每個泛型參數的約束

然後是6個命名堆:

堆 說明 #String 一個AscII string數組,被元數據表所引用,來表示方法名、字段名、類名、變量名以及資源相關字符串,但不包含string literals。 #Blob 包含元數據引用的二進制對象,但不包含用戶定義對象 #US 一個unicode string數組,包含了定義在代碼中的字符串(string literals),這些字符串可以直接由ldstr指令加載獲取,還記得嗎?我們在《第二十二回:字符串駐留(上)---帶著問題思考》中對字符串創建過程的論述嗎? #GUID 保存了128byte的GUID值,由元數據表引用 #~ 一個特殊堆,包含了所有的元數據表,會引用其他的堆。 #- 一個未壓縮的#~堆。除了#-堆,其他堆都是壓縮的。

Note:對於#String和#US,一個簡單的區別就是:

string hello = "Hello, World";

變量hello名,將保存在#String,而代碼中字符串信息“Hello, World”則被保存在#US中。

關於元數據信息的詳細描述,例如每個表包含哪些列,不同表間的關系,請參考[Standard ECMA-335]和[The .NET File Format]。

在PE文件格式中,Metadata有著復雜的結構,我試圖以數據庫管理數據的角度出發來理解元數據的結構和關系,所以表示元數據的邏輯結構被成為元數據表,類似於數據庫表有主鍵和Sechema,元數據表以RID(表索引)和元-元數據表示類同的概念,以TypeDef表為例,通過數據引用關系同時與Field、Method、TypeRef等表發生關聯,其他表間又有類似的關系,從而形成一個復雜的類數據庫結構:

因此,元數據是保存了類型的編譯後數據,是.NET程序運行的基礎,我們可以在運行時動態的以反射的方式獲取元數據信息,而這些信息在.NET Framework中以System.Type、MethodInfo等封裝,例如截取MSDN中一個類間關系的簡單示例:

對於每個CLR類型而言都可以通過Object.GetType方法返回其Type,從而任意的取到所有的運行時元數據信息:

// Release : code04, 2009/02/21
// Author : Anytao, http://www.anytao.com
// List  : Program.cs
private static void ShowMemberInfo()
{
   var assems = AppDomain.CurrentDomain.GetAssemblies();

   foreach (Assembly ass in assems)
   {
     foreach (Type t in ass.GetTypes())
     {
       foreach (MemberInfo mi in t.GetMembers())
       {
         Console.WriteLine("Name:{0}, Type:{1}", mi.Name, mi.MemberType.ToString());
       }
     }
   }
}

執行上述方法,將獲取一個長長的列表,看到很多熟悉的符號:-)

4 IL是什麼?

IL,又稱為CIL或者MSIL,翻譯為中文就是中間語言,由ECMA組織(Standard ECMA-335)提供完整的定義和規范。顧名思義,中間語言正如它的名稱所言,任何與CLR兼容的編譯器所生成的都是中間語言代碼,這是實現CLR跨語言的基礎結構之一。IL就像一座橋梁,其指令集獨立於CPU指令而存在,可以由JIT編譯器在運行時翻譯為本地代碼執行,連接了任何遵守CLS規范的高級語言,為.NET平台提供了最基本的支持。在[你必須知道的.NET]一書中,我用一整章(第3章 “一切從IL開始”)的篇幅對IL的基本內容進行了相應的介紹,所以關於IL的基礎內容例如基本類型、IL分析方法、常見指令、基本運算等,就不在本文有所贅述,只對IL基本內容進行一點小結:

IL是一種面向對象的機器語言,因此具有面向對象語言的所有特性,類、對象、繼承、多態等仍然是IL語言的基本概念。

IL指令獨立於CPU指令,CLR通過JIT編譯機制將其轉換為本地代碼。

IL和元數據是了解CLR運行機制的重要內容,對於我們打開CLR神秘面紗有著重要的意義。

如前文[初次接觸]部分論述的一樣,可以通過ILDasm.exe或者Reflector工具對托管代碼執行反編譯來查看其IL代碼,對於很多情況下IL代碼分析可以解決很多高級語言隱藏的語法糖游戲,例如C#3.0提出的自動屬性、隱式類型、匿名類型、擴展方法等都可以很快從IL分析中找到答案,所以適當的了解IL是必要的。那麼我們在下面JIT編譯時的一個片段來了解IL代碼對於托管程序執行的作用。

另外,Metadata描述了靜態的結構,而IL闡釋了動態的執行,而IL代碼是通過一個4字節大小的地址引用元數據表的。該引用被稱為元數據符號(Metadata Token,也就是記錄元數據表的位置信息),在ILdasm.exe工具中選中“Show token values”,就可以在IL代碼中看到IL代碼通過Metadata Token引用元數據表的情況:

.method /*06000003*/ private hidebysig static
     void Main(string[] args) cil managed
{
  .entrypoint
  // Code size    36 (0x24)
  .maxstack 2
  .locals /*11000002*/ init ([0] int32 id,
       [1] class Anytao.Insidenet.MetadataIL.One/*02000004*/ one,
       [2] class Anytao.Insidenet.MetadataIL.Two/*02000002*/ two)
  IL_0000: nop
  IL_0001: ldc.i4.1
  IL_0002: stloc.0
  IL_0003: newobj   instance void Anytao.Insidenet.MetadataIL.One/*02000004*/::.ctor() /* 06000007 */
  IL_0008: stloc.1
  IL_0009: ldloc.1
  IL_000a: ldloc.0
  IL_000b: callvirt  instance void Anytao.Insidenet.MetadataIL.One/*02000004*/::set_ID(int32) /* 06000006 */
  IL_0010: nop
  IL_0011: newobj   instance void Anytao.Insidenet.MetadataIL.Two/*02000002*/::.ctor() /* 06000002 */
  IL_0016: stloc.2
  IL_0017: ldloc.2
  IL_0018: callvirt  instance string Anytao.Insidenet.MetadataIL.Two/*02000002*/::SayHello() /* 06000001 */
  IL_001d: call    void [mscorlib/*23000001*/]System.Console/*01000012*/::WriteLine(string) /* 0A000011 */
  IL_0022: nop
  IL_0023: ret
} // end of method Program::Main

其中,按照ECMA定義的規范,元數據第一個字節表示引用的元數據表,而其余三個字節則表示在相應元數據表中的記錄,例如06000003表示了引用了MethodDef(06)表的000003項Main方法。

我們可以通過Type的MetadataToken屬性在運行時反射獲取類型的元數據符號,例如:

static void Main(string[] args)
{
   Console.WriteLine(typeof(One).MetadataToken);
}

有了上述所有的准備,我們就可以著手分析元數據和IL在程序執行時的角色和關聯。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved