[你必须知道的.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.cnblogs.com/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(上)]