CustomAttribute表描述了该Assembly中的自定义属性,包含要实例化一个自定义属性所需要的所有信息。这个表有以下域构成,Parent是个HasCustomAttribute类型的Coded Token,Type是个CustomAttributeType类型的Coded Token;Value是个指向#Blob流的索引。一个CustomAttribute有6个Byte。
本例中有一个CustomAttribute,2E 00/0B 00/39 00。

StandAloneSig表只有一个指向#Blob流的Signature。2个Byte。对大多数使用signature的情况,如Field.Signature, Method.Signature等等Signature是指向保存在Blob流的一个signature数据块。而对于一些特殊情况,Blob中保存的Signature数据可能不被任意一个结构元素所引用,例如IL指令集中calli指令(间接函数调用指令,使用函数指针而非指向方法的Token调用,往往用于调用Native方法)需要一个Signature描述其调用的函数指针的类型。此时就需要一个StandAloneSig表项,指向这个孤儿signature。
本例中有两个StandAloneSig,值为:2A 00 34 00。

PropertyMap表是一个映射表,负责把PropertyDef中定义的属性映射到其归属的TypeDef表定义的类型中,这样就可以将属性的归属信息从类型和属性的定义中完全抽象出来。它有如下字段:2个Byte的指向TypeDef索引的Parent;2个Byte的指向Property表的 PropertyList;因此一个PropertyMap有4个Byte。
本例中有一个PropertyMap,值为:02 00 01 00。Parent的值是0x0002,指向的Echo类的定义;PropertyList的值是0x0001,指向的是EchoString属性的定义,合并起来,意思是说在Echo类中定一个了一个Property,名为EchoString。

Property表描述了该Assembly中的属性定义。该表有如下字段:2个Byte的Flags;指向#Strings流的Name索引;指向#Blob流的Type索引。Flags的定义也在Corhdr.h中,名为CorPropertyAttr。每个Property占用6个Byte。
本例中有一个Property,00 00 68 00 1A 00,Flags为0x0000,Name值为0x0068,是“EchoString”;Type值为0x001A,值为06。

MethodSemantic表用于将属性或事件映射到其实现代码所在的方法上。一个MethodSemantic包含如下Field:2个Byte的Semantic,2个Byte的指向Method表的索引;2个Byte的Association是HasSemantic类型的Coded Token。这样一个表就有6个Byte。Semantic的值定义在Corhdr.h中,如下:

// MethodSemantic attr bits, used by DefineProperty, DefineEvent.
typedef enum CorMethodSemanticsAttr
{
msSetter = 0x0001, // Setter for property
msGetter = 0x0002, // Getter for property
msOther = 0x0004, // other method for property or event
msAddOn = 0x0008, // AddOn method for event
msRemoveOn = 0x0010, // RemoveOn method for event
msFire = 0x0020, // Fire method for event
} CorMethodSemanticsAttr;

本例中一共有两个MethodSemantic,分别是:02 00/01 00/03 00|01 00/02 00/03 00。分析第一个,Semantic的值为0x0002,意思是属性的Getter,Method的值是0x0001,指向Method表的第一项,是“get_EchoString”;Association的值是0x0003,解码后是:0x17000001,指向Property表的第1项。

Assembly是每个Assembly必有的表,描述了本Assembly的一些信息。它有如下的描述项:4个Byte的HashAlgID,说明了此Assembly中所用的Hash算法,定义如下:
public enum AssemblyHashAlgorithm
{
None = 0,
MD5 = 0x8003,
SHA1 = 0x8004
}
然后是版本信息,我们知道在.NET中,版本有四个部分构成,MajorVersion,MinorVersion,BuildNumber,RevisionNumber。这部分信息就储存在这里,每个部分占两个Byte。接下来是4个Byte的Flags。2个Byte的指向#Blob的PublicKey,然后是两个指向#String的索引,分别是Name和Locale。那么,一个Assembly表项占22个字节。
本例中的Assembly的值为:04 80 00 00/00 00/00 00/00 00/00 00/00 00 00 00/00 00/9F 00/00 00。我们分析一下,本例中Assembly的Hash算法为0x8004(SHA1)。版本信息为:0.0.0.0。Flags为0x000000;Public Key为0x00;Name是0x009F“hello”;Locale为0x0000。

AssemblyRef表描述了本Assembly引用的其他Assembly。AssemblyRef的组成基本与Assembly差不多:4个两Byte的版本号,然后是4个Byte的Flags,指向#Blob的PublicKeyOrToken;指向#Strings的Name和Locale。和指向#Blob的HashValue,一个AssemblyRef表项占20个Byte。
本例有一个AssemblyRef表:01 00/00 00/E4 0C/00 00/00 00 00 00/01 00/14 00/00 00/00 00。表示本Assembly引用了一个版本号为1.0.3300.1,Flags为0x00000000,名字为“mscorlib”的程序集。

总结:
至此,这个简单的Hello World程序的可执行文件的格式分析就告一段落了。我们从分析中可以看到,与传统的C/C++程序不同,.NET下的程序经过编译器编译之后的可执行文件中不仅仅包含了程序的可执行代码,还包含了大量的对该段代码的描述信息。这样,就使程序在运行的时候得到代码的描述信息成为可能,这也就是所谓的“自描述”。有了自描述的特性,使.net下的程序非常有利于通过网络运行,为实现RPC等提供了非常坚实的底层支持。自描述的优点是不用多说了,缺点么,反汇编容易了不少,知识产权哪 😀 ……
但是,在本文中一共只有14种表,还有很多SSCLI中的表没有涉及到,例如事件。此外,原数据只是.NET代码信息的静态描述,在运行时,.NET通过Class Loader把程序集加载到内存,有另外一套动态的描述代码的方式(SSCLI中的EEClass和MethodTable等),并且提供了System.Reflection命名空间为程序员提供编程接口。如果有时间,我会接着分析Class Loader的代码。
除了System.Reflection之外,微软还提供了一套称之为“Manifest API”的非托管编程接口,用来访问,分析和生成元数据,这些API基本上是基于COM接口的,CLR虚拟机访问MetaData基本上也是使用这些接口。但微软声称这是给开发工具(例如其他语言的编译器)的程序员用的。SSCLI中cli\src\md目录下的代码基本上就是Manifest API的实现代码。
在新的Visual Studio “Whidbey”发行之后,微软会发布下一个版本的SSCLI。不知道变化会有多大呢?