代码改变世界

【读书笔记】.NET本质论第二章-Components(Part One)

2009-04-18 15:50  横刀天笑  阅读(...)  评论(... 编辑 收藏

所有为CLR编写的程序都放在modules(模块)中,module包括CIL、Metadata、Resource。CIL里保存的就是程序的代码,Metadata描述模块内定义的类型、方法啊、所依赖的类型等方方面面的信息,可以说将module描述的个清清楚楚。Resource里可以包括字符串、图片等,就是一些程序需要使用的资源都打包放在这里。

CLR模块是一个字节流,使用扩展的PE/COFF可执行文件的格式,因此也是一个有效的Win32模块,可以使用LoadLibrary API调用。下图就是CLR模块的结构:

clrmodule

既然CLR模块也是一个PE/COFF模块,我们就来看看有什么区别,我们使用PEView这个工具,看看Win32模块和CLR模块有什么不同,第一个是notepad.exe,第二个是我使用C#编写的一个控制台程序:

win32 clr

差别主要就在于CLR模块在.text段多了一个CLI Header,这个就是上图的IMAGE_COR20_HEADER结构,强大的PEView也可以看出这个结构的详细内容:

clrheader

我们甚至可以从Rotor(这是微软为了科研和研究的目的,开放源代码的一份CLI实现)的源代码中找到这个结构的详细定义:

// COM+ 2.0 header structure.
typedef struct IMAGE_COR20_HEADER
{
    // Header versioning
    DWORD                   cb;             
    WORD                    MajorRuntimeVersion;
    WORD                    MinorRuntimeVersion;
    
    // Symbol table and startup information
    IMAGE_DATA_DIRECTORY    MetaData;        
    DWORD                   Flags;           
// DDBLD - Added next section to replace following lin
// DDBLD - Still verifying, since not in NT SDK
//    DWORD                   EntryPointToken;
  
    // If COMIMAGE_FLAGS_NATIVE_ENTRYPOINT is not set, EntryPointToken represents a managed entrypoint.
    // If COMIMAGE_FLAGS_NATIVE_ENTRYPOINT is set, EntryPointRVA represents an RVA to a native entrypoint.
    union {
        DWORD               EntryPointToken;
        DWORD               EntryPointRVA;
    };
// DDBLD - End of Added Area
    
    // Binding information
    IMAGE_DATA_DIRECTORY    Resources;
    IMAGE_DATA_DIRECTORY    StrongNameSignature;

    // Regular fixup and binding information
    IMAGE_DATA_DIRECTORY    CodeManagerTable;
    IMAGE_DATA_DIRECTORY    VTableFixups;
    IMAGE_DATA_DIRECTORY    ExportAddressTableJumps;

    // Precompiled image info (internal use only - set to zero)
    IMAGE_DATA_DIRECTORY    ManagedNativeHeader;
    
} IMAGE_COR20_HEADER, *PIMAGE_COR20_HEADER;
下面我就对这个结构详细解释一下。
cb,这个字段用来表示这个结构的大小(字节数),常见的值是0x48(72)个字节
MajorRuntimeVersion,MinorRuntimeVersion就是用来描述.NET运行时的版本了。看看上面PEView查看CLI Header的那张图,显示的运行时版本主版本不
是2,次版本不是5么,这不正好对应着.NET 2.0的版本号:v2.0.50727。
MetaData就是用来描述模块的元数据的位置,就是一偏移地址,通过这个运行时就可以定位元数据了。
Flags是个标记,它的值可以是:
COMIMAGE_FLAGS_ILONLY 			= 0x00000001,
COMIMAGE_FLAGS_32BITREQUIRED 		= 0x00000002,
COMIMAGE_FLAGS_IL_LIBRARY 		= 0x00000004,
COMIMAGE_FLAGS_TRACKDEBUGDATA 	        = 0x00010000
看到名字就差不多知道这几个标记值表示啥意思了。大部分的CLR模块值都是1。
EntryPointToken这就是表示CLR模块的入口点方法了,大部分时候就是Main方法。
后面的几个字段,类型都是IMAGE_DATA_DIRECTORY,由此可以看出都是一些偏移值。
 

使用编译器编译代码时,使用/t:module编译,默认就会生成一个后缀名为.netmodule的文件,这个文件就是单纯的module,module不能独立的部署,也就是说不能被加载执行,因为它还缺少程序集清单,要加载module,必须将它与程序集相关联。

使用/t:library,/t:exe,/t:winexe都能生成程序集。

使用/t:exe和/t:winexe编译的程序集必须有一个入口点方法(C#):

static void Main(){}
static void Main(string[] args){}
static int Main(){}
static int Main(string[] args){}

入口点方法只能有这几种方式,而且它可以为私有的,必须放在一个类型中。

Assembly

刚才说了,要想部署一个CLR的module,首先的将其与一个assembly关联。而assembly是一个或多个module的逻辑集合,注意这个逻辑。也就是说,一个程序集可以是一个文件,也可以是多个文件(要注意的是,Visual Studio只能生成单文件的程序集,所以这往往给我们带来误解,以为程序集就是一个文件,其实程序集是个逻辑概念,与物理的文件没啥必然关系)。

注意:如果两个程序集同时引用一个公共的module,CLR会将这个公共的module作为两个不同的module对待。

Assembly是CLR中部署的原子,用于打包、加载、分布已经实施版本控制的单元。module却并不是这样的单元。

要让CLR能找到程序集中的各个部分,程序集的元数据中还得包括一个称之为assembly manifest(程序集清单)的东东。这个manifest中记录了程序集包括的所有模块。因为module缺少这样的manifest,所以module不能直接被加载,只能通过有manifest的模块的引用而加载。

这个程序集清单很重要,首先他跟程序集捆绑在一起,不会丢掉,所以只需要加载了程序集,程序集清单也就加载进来了,通过程序集清单我们就可以找出这个程序集中所使用的类型身在何方。

Assembly Name

每个程序集的名字有四个部分,用来唯一的标识该程序集:friendly name,culture,developer,version。这个名字会保存在程序集清单中(自己和引用它的)。.NET中使用System.Reflection.AssemblyName这个类来表示程序集的名称。System.Reflection.Assembly的GetName()方法就返回一个AssemblyName类型的实例。

Name

AssemblyName的Name属性就对应于程序集文件名(程序集清单所在的文件)。程序集名称的四个部分只有friendly name不是可选的,其余几个都可选。

Version Number

程序集名种还包括Version Number:Major.Minor.Build.Revision(主版本号.次版本号.构建版本号.修正版本号)。版本号是在构建的时候指定的,一般使用自定义的特性在源代码中指定(AssemblyVersion特性)。

Attribute Parameter Actual Value
1 1.0.0.0
1.2 1.2.0.0
1.2.3 1.2.3.0
1.2.3.4 1.2.3.4
1.2.* 1.2.d.s
1.2.3.* 1.2.3.s
<未提供> 0.0.0.0
d是从2000年2月1日到现在的天数 s是从午夜到现在的秒数/2

CultureInfo

使用AssemblyCulture特性指定。Culture的字符串一般有两部分:第一部分是两个小写的代码,第二部分表示区域的代码大写。比如“en-US”表示美国英语。包含CultureInfo的程序集不能包含代码,只包含resource,也就是所谓的卫星程序集。比如包含语言的字符串、界面元素等等。包含代码的程序集应该是语言中心的。

Public Key

这个是用来标识组件的开发者的。可以使用128位的public key或8位的public key token。这个用来避免两个公司产生同名的程序集发生冲突,有了这个public key就不会了(除非你们公司的private key被泄露了)。

因为引用这个程序集时,通常要手写出这个程序集的名字,所以可以用一个字符串表示这个AssemblyName。这个就是程序集的display name。用逗号分隔,以Name开始,后面跟着的可选。如果四部分全部指定就称之为fully qualified reference。如果只指定了一部分,比如没有指定语言文化或PublicKeyToken,那么使用这个名字就是partially qualified reference,一般应该避免使用partially qualified reference,但是你也可以通过配置文件将部分指定的名称限定到完全限定名的程序集,如下所示:

<configuration>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <qualifyAssembly partialName="math" 
                         fullName="math,version=1.0.0.0,publicKeyToken=a1690a5ea44bab32,culture=neutral"/>
      </assemblyBinding>
   </runtime>
</configuration>

注意,如果程序集不包含CultureInfo,则必须指定为Culture=neutral,如果没有提供Public key,则指定为PublicKeyToken=null。注意,没有指定(省略)和指定了为null本质上是不同的。