说在,开篇之前 |
书接上回[第二十四回:认识元数据和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: nopIL_0001: ldc.i4.1IL_0002: stloc.0IL_0003: newobj instance void Anytao.Insidenet.MetadataIL.One/*02000004*/::.ctor() /* 06000007 */IL_0008: stloc.1IL_0009: ldloc.1IL_000a: ldloc.0IL_000b: callvirt instance void Anytao.Insidenet.MetadataIL.One/*02000004*/::set_ID(int32) /* 06000006 */IL_0010: nopIL_0011: newobj instance void Anytao.Insidenet.MetadataIL.Two/*02000002*/::.ctor() /* 06000002 */IL_0016: stloc.2IL_0017: ldloc.2IL_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: nopIL_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的创业产品Worktile
Worktile,新一代简单好用、体验极致的团队协同、项目管理工具,让你和你的团队随时随地一起工作。完全免费,现在就去了解一下吧。
https://worktile.com
参考文献
- 《你必须知道的.NET》第3章 “一切从IL开始”
- DonBox,《.NET本质论》
- http://www.sloppycode.net/articles/inside-net-assemblies-and-metadata.aspx
- http://www.codeproject.com/KB/dotnet/dotnetformat.aspx
支持(0) 反对(0)
#10楼 2009-02-25 09:57 yy125
关注中,元数据和IL是不是和语言无关呢,我用vb写,是不是也生成一样的元数据和IL呢
支持(0) 反对(0)
#11楼 [楼主] 2009-02-25 10:06 Anytao
@yy125
对,这是.NET跨语言的基础,不同的高级语言(基于.NET平台,这是前提)由各自编译器编译之后生成的元数据和IL是统一的。
支持(0) 反对(0)
#12楼 2009-02-25 23:45 未登录的包建强
anytao,你这篇写得确实不错,干净利落,一针见血。
下面说但是,
你用反射来说明元数据,有点自圆其说。我建议用BinaryStream,代码如下:
FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open);
BinaryReader r = new BinaryReader(s);
byte a, b;
a = r.ReadByte();
b = r.ReadByte();
哥哥我就是要一个字节一个字节地去读exe文件,当然,这招儿也是从老外那里学来的。
参考文献:http://www.cnblogs.com/Jax/archive/2009/01/02/1366888.html
支持(0) 反对(0)
#13楼 2009-02-25 23:54 未登录的包建强
@Anders
《.NET 设计规范--.NET约定、惯用法与模式》,这本书我有,回头我送你好了,算是给我干女儿的见面礼。
@胖赵
就是来搅黄你生意的。娃哈哈。。。
支持(0) 反对(0)
#14楼 [楼主] 2009-02-26 10:26 Anytao
@未登录的包建强
不知道说啥了:-)
支持(0) 反对(0)
#15楼 2009-02-26 22:54 波波塔
#String 一个AscII string数组 ...
如果 string 中文test="中文等也是用AscII 存储在 #string里面的吗?
支持(0) 反对(0)
#16楼 2009-03-04 21:15 JacksonLin
严重支持
支持(0) 反对(0)
#17楼 2009-12-22 14:49 飛翔的小豬
=============================================
•引用表,描述了源代码中引用的类型和成员信息,引用元素可以是同一程序集的其他模块,也可以是不同程序集的模块,主要包括:AssemblyRef、TypeRef、ModuleRef、MethodsRef等。
=============================================
引用表中应该不包括MethodsRef,不存在单独的FieldRef和MethodRef,因为MemberRef包括了指向字段和方法的引用,你的44个CLR元数据表中也没有包含,不知道是不是笔误。
看你的文章要比看专著来的轻松得多。
小弟班门弄斧了。