[你必須知道的.NET]第二十五回:認識元數據和IL(中)
[你必須知道的.NET]第二十五回:認識元數據和IL(中)
發布日期:2009.02.25 作者:Anytao
? 2009 Anytao.com ,Anytao原創作品,轉貼請注明作者和出處。
| 書接上回[第二十四回:認識元數據和IL(上)],我們對PE文件、程序集、托管模塊,這些概念與元數據、IL的關系進行了必要的鋪墊,同時順便熟悉了以ILDASM工具進行反編譯的基本方法認知,下面是時候來了解什么是元數據,什么是IL這個話題了,我們繼續。 很早就有說說Metadata(元數據)和IL(中間語言)的想法了,一直在這篇開始才算腳踏實地的對這兩個階級兄弟投去些細關懷,雖然來得沒有《第一回:恩怨情仇:is和as》那么迅速,但是Metadata和IL卻是絕對重量級的內容,值得我們在任何時間關注,本文就是開始。 www.anytao.com |
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在程序執行時的角色和關聯。
|
anytao | ? 2009 Anytao.com | http://www.rzrgm.cn/anytao/archive/2009/02/25/must_net_25.html
2009/02/25 | http://anytao.cnblogs.com/
原文地址:
本文以“現狀”提供且沒有任何擔保,同時也沒有授予任何權利。 | This posting is provided "AS IS" with no warranties, and confers no rights.
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
參考文獻
- 《你必須知道的.NET》第3章 “一切從IL開始”
- DonBox,《.NET本質論》
- http://www.sloppycode.net/articles/inside-net-assemblies-and-metadata.aspx
- http://www.codeproject.com/KB/dotnet/dotnetformat.aspx
溫故知新
[開篇有益]
[第一回:恩怨情仇:is和as]
[第二回:對抽象編程:接口和抽象類]
[第三回:歷史糾葛:特性和屬性]
[第四回:后來居上:class和struct]
[第五回:深入淺出關鍵字---把new說透]
[第六回:深入淺出關鍵字---base和this]
[第七回:品味類型---從通用類型系統開始]
[第八回:品味類型---值類型與引用類型(上)-內存有理]
[第九回:品味類型---值類型與引用類型(中)-規則無邊]
[第十回:品味類型---值類型與引用類型(下)-應用征途]
[第十一回:參數之惑---傳遞的藝術(上)]
[第十二回:參數之惑---傳遞的藝術(下)]
[第十三回:從Hello, world開始認識IL]
[第十四回:認識IL代碼---從開始到現在]
[第十五回:繼承本質論]
[第十六回:深入淺出關鍵字---using全接觸]
[第十七回:貌合神離:覆寫和重載]
[第十八回:對象創建始末(上)]
[第十九回:對象創建始末(下)]
[第二十回:學習方法論]
[第二十一回:認識全面的null]
[第二十二回:字符串駐留(上)---帶著問題思考]
[第二十三回:品味細節,深入.NET的類型構造器]
[第二十四回:認識元數據和IL(上)]
Worktile,新一代簡單好用、體驗極致的團隊協同、項目管理工具,讓你和你的團隊隨時隨地一起工作。完全免費,現在就去了解一下吧。
https://worktile.com

浙公網安備 33010602011771號