[你必须知道的.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在程序执行时的角色和关联。

欲知后事如何,且听下文继续:-)

                                                                                     www.anytao.com


 

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.

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

参考文献

posted @ 2013-07-20 11:51  沙耶  阅读(1654)  评论(0编辑  收藏  举报