(翻译)《Expert .NET 2.0 IL Assembler》 第一章 简单示例 1.1 CLR的基本概念
这一章提供了对ILAsm——MSIL编译语言的基本概述。(MSIL表示Microsoft中间语言,稍后将在本章讨论。)本章回顾了一个用ILAsm写的相对简单的程序,随后我建议作一些改动来举例说明:如何用ILAsm表示Microsoft .NET程序中的概念和元素。
这一章并没有教你如何用ILAsm写程序。但是这将帮助你理解ILAsm和ILDASM做了什么,以及如何通过你的这些理解并借助这些无处不在的工具来分析基于.NET程序的内部结构。你也将会认识到一些有趣的事情,在CLR中发生在场景背后的神秘事件,相当的有趣,我希望这将促使你读完本书的剩余部分。
注意:方便起见,我在本书中将IL汇编语言简写为ILAsm。不要将其与ILASM混淆,后者是IL编译器在.NET文档中的简写形式。
CLR的基本概念
.NET CLR不仅是.NET诸多方面中的一种,更是.NET的核心。(注意到,因为语义多样性的缘故,,我有时会将CLR称为运行时。)我将重点放在.NET中真正发生行为的部分,而不是关于.NET平台的全部描述。
注意:获取关于.NET以及它的组件的一半结构的精彩讨论,参见David S.Platt所著《Introducing Micorsoft .NET》第三版(Microsoft Press, 2003),以及Tom Archer和Andrew Whitechapel合著的《Inside C#》第二版(Microsoft Press, 2002)。
简而言之,CLR是.NET应用程序运行时所在的一个运行环境。它提供了介于.NET应用程序和底层操作系统中间的一个可操作的层。从概念上说,CLR类似于解释型语言(如GBasic)。但是这种相似性只是概念上的:CLR不是一种解释器。
由面向.NET的编译器(如Microsoft Visual C#,Microsoft Visual Basic.NET,ILAsm以及很多其它编译器)生成的.NET应用程序,表现为一种抽象的中间形式,而不依赖于原始的编程语言、目标机器以及它的操作系统。因为它们可以表现为这种抽象的形式,用各种语言写成的.NET应用程序能够紧密地互操作,不仅是可以互相调用方法这一级别,还有类的继承性这一级别。
当然,既然给出这些程序语言的不同之处,那么就有必要为应用程序建立一套使这些语言配合融洽的规则。例如,如果你用C#写一个应用程序,命名3个条目:MYITEM、MyItem、myitem;而Basic.NET对大小写不敏感的,在分辨它们的时候就很困难。同样地,如果你用ILAsm写一个程序,定义了一个全局方法,C#将不能调用这个方法,因为C#中是没有 “全局”这个概念的。
这套规则保证了.NET应用程序的互操作能力,被称为公共语言规范(CLS),它的轮廓会在CLI这一ECMA国际和ISO标准的第一部分描述。CLS限制了命名转换、数据类型、函数类型以及其他特定的元素,为不同的语言形成一个共同点。然而,要牢记的很重要的一点,CLS只是一种建议而并没有在CLR上生成什么功能,但是你不能保证它可以在所有级别与其它应用程序互操作。
作为专为CLR环境设计的.NET应用程序,这种抽象的中间表示,包括了两个主要部分:元数据和托管代码。元数据是应用程序的所有结构化元素的描述符组成的体系,包括类,以及它们的成员和特性、全局项等等;元数据也包括这些项之间的关系。本章提供了元数据的一些例子,后面的章节将会描述所有的元数据结构。
托管代码代表了应用程序的方法的功能性,它们编码在一个抽象的二进制格式中,后者被称为微软中间语言(MSIL)或公共中间语言(CIL)。简而言之,我将这种编码简称为中间语言(IL)。当然,世界上还存在着其它中间语言,但是我们现在所关心的是,我们都认为IL就是MSIL,除非特别指出。
CLR“管理着”IL代码。CLR管理包括(但并不局限于此):类型控制,结构化异常处理,以及垃圾收集。类型控制包括了在执行期间对的元素类型的验证和转换。托管的异常处理从机能上类似于“非托管”的结构化异常处理,但它是由CLR执行的而不是操作系统。垃圾收集包括了自动识别和处理不再使用的对象。
一个作为专为CLR环境设计的.NET应用程序,由一个或多个托管可执行体组成,其中每一个都携带元数据和(可选的)托管代码。托管代码是可选的,这是因为,创建一个不含任何方法的托管可执行体是可能的。(显然,这样一个可执行体只能用来作为一个应用程序的附属部分。)托管的.NET应用程序称为“程序集”。(这样的陈述有点简单;为了获取更多关于程序集和应用程序的信息,参见第6章。)托管的可执行体涉及到模块(module)。你可以创建单模块的程序集和多模块的程序集。正如图1-1所示,每一个程序集包括了一个主模块,这个模块在它的元数据中携带了程序集的验证信息。
图1-1 一个多模块的.NET程序集
图1-1还展示了托管可执行体的两个主要部分:元数据和IL代码。两个主要的CLR子系统:加载器和JIT(just-in-time)编译器,各自相应地处理了每个部分。
简单地讲,加载器读取元数据并在内存中创建类及其成员的内部表示和布局。它是按需执行的,这意味着一个类只有在被引用的时候才会被加载和表示。没有引用到的类不会被加载。当加载一个类的时候,加载器对相关元数据运行一系列的一致性检查。
JIT编译器,依赖于加载器活动的结果,将IL编码的方法编译到底层平台的本地代码中。因为CLR并不是一个解释器,它不执行任何IL代码。替代地,IL代码在内存中被编译到本地代码中,这些本地代码会被执行。JIT编译器也是按需执行的,这意味着一个方法只有在被调用的时候才会被编译。编译器方法在内存中留有一个缓存。如果内存是有限的,毕竟,存在小型计算设备这样的例子,如一个PDA处理器或一个智能电话。如果这些方法没有被使用到,就会被销毁。如果一个方法在被销毁后被再次调用,那么该方法会被重新编译
图1-2展示了创建和执行一个托管的.NET应用程序的顺序。顶部的带有空心圆的箭头指出了数据传输;带有黑色实心圆的箭头代表了请求和控制信息。
图1-2创建和执行一个托管的.NET应用程序
你可以使用NGEN工具,预编译一个托管的可执行体,从IL到本地代码。你可以这么做,当可执行体重复运行在本地硬盘的时候,从而节省了JIT编译器上花费的时间。这是一个标准的过程,例如,对于.NET框架的托管部分,将会在安装过程中进行预编译。(Tom Archer称之为“安装期代码生成”)。在这种情况下,预编译代码保存到硬盘或其他存储介质,每次可执行体被调用的时候,将使用预编译的本地代码版本,而不是原始的IL版本。而原始的文件也必须存在,这是因为预编译版本在被允许执行之前,必须通过原始文件进行鉴别。
随着元数据和IL代码的角色的确定,我将向你介绍使用ILAsm来描述它们的方式。