CLR Via C 第一章 CLR基础
第一章 CLR执行模型
.NET框架介绍了许多新的概念和技术。我在这一章的目地是向你介绍关于.NET框架是怎样被设计的一个概述,还有框架中包含的一些新技术,并且定义许多你将会用到的一些条目。我也会介绍把源代码编译成一个应用程序或一系列包含类型(classes,structures)的组件的过程,并且解释你的应用程序是怎样执行的。
1.1将源代码编译成托管模块
好了,你已经决定使用.NET框架作为你的开发平台。棒极了!你的第一步就是决定你打算生成哪种类型的应用程序或组件。让我们假设你已经完成这个细节;所有事情都已经设计完了,规格也写完了,你已经打算开始你的开发。
现在你必须决定你要使用哪种语言。这个任务通常很困难,因为不同语言提供了不同的能力。举个例子,在非托管C/C++中,你拥有漂亮的低等级系统控制。你可以精确地管理内存,如果你需要可以很容易的创建线程等等。Microsoft VB6.0,在另一方面,允许你快速地生成UI应用程序并且你可以轻易的控制COM对象和数据库。
公共语言运行时(CLR)顾名思义:是指可被各种不同编程语言使用的运行时。CLR的核心特性可用于所有面向它的编程语言。
例如,运行时使用异常报告错误,因此所有面向运行时的语言都可以通过异常获得错误的报告。另一个例子是说运行时允许你创建线程,所以所有面向运行时的语言都可以创建线程。
实际上,在运行时,CLR并不知道开发人员使用哪个语言完成源代码。这就意味着你无论选择哪种语言来编写代码都会变得很轻松。你可以用任何语言编写代码,只要编译器可以编译面向CLR的代码。
所以,如果我所说的是真的话,使用一种语言,这种语言和其他语言比起来优势在哪?好吧,我觉得编译器是语法检查器和“正确代码”分析器。它们检查你的源代码,确保你编写的有意义,并且输出描述你意图的代码。不同的语言允许你通过使用不同的的语法开发。不要低估这个选择的价值。举个例子来说,对于数学或者金融类的应用程序,使用APL语言比使用perl语言可以多节省许多的开发时间。
Microsoft已经推出了多种面向运行时的语言编译器:C++/CLI,C#,Visual Basic,JScript,J#和一个中间语言汇编器(Intermediate Language Assembler)。例外,一些公司,大学也推出面向运行时的代码产品,我知道的有Ada,APL,Caml,COBOL,Eiffel,Forth,Fortran,Haskell,Lexico,LISO,LOGO,Lua,Mercury,ML,Mondrian,Oberon,Pascal,Perl,Php,Prolog,Python,RPG,Scheme,Smalltalk,和Tcl/Tk。
下图显示了编译源代码文件的过程。从这个图上看出,你可以使用任何一种支持CLR语言编写源代码文件。然后相应的编译器将检查语法,并分析源代码。不管你使用何种编译器,结果都是一个托管模块。托管模块是一个标准的32位Microsoft Windows可移植执行文件(PE32),或者是一个需要CLR去执行的标准64位Microsoft Windows可移植执行文件(PE32+)。
托管模块的组成:
- PE32或者PE32+ header:标准的windows PE文件头部,和Common Object File Format(COFF)头部类似。如果这个头部使用PE32格式,这个文件可以在32位或64位版本的windows上运行。如果这个头部使用PE32+格式,这个文件需要在64位版本的windows上运行。这个头部说明了这是一个什么类型的文件:GUI,CUI或者是DLL,并且包含一个说明这个文件是何时创建的时间戳。对于那些只包含IL代码的模块,PE32(+)中的大量信息都会被忽略。对于那些包含本地CPU代码的模块,这个头部还要包含关于本地CPU代码的信息。
- CLR header:包含了使其成为托管模块的信息(可以被CLR或其他工具解析)。这个头部包括托管模块所需要的CLR版本号,一些标记,托管模块入口点方法(main方法)的MethodDef元数据标记、以及有关托管模块的元数据(metadata)、资源(resources)、强命名(strong name)、标记(flags)、和其他一些意义不大的信息的位置和大小。
- Metadata:每个托管模块包含了元数据表。元数据表主要分两种:一种描述在源代码中定义的类型和成员,另一种描述源代码中引用的类型和成员。
- Intermediate Language(IL) code:编译器在编译源代码时产生的代码。在运行时,CLR将IL编译为本地CPU指令。
大多数早期编译器产生的代码都是面向特定CPU架构的,像x86,x64或者IA64。而所有CLR兼容的编译器所生成的都是IL代码。IL代码也被称为托管代码(managed code),因为CLR管理其执行。
除了生成IL,所有面向CLR的编译器都需要把为每个托管模块生成一个完整的元数据(metadata)。简要的说,元数据(metadata)是一个数据表的集合,这些数据表有一些描述托管模块所定义的内容(如定义的类型和它们的成员),此外还有一些描述托管模块所引用的内容(如引用的类型和它们的成员)。元数据是一些早期技术如类型库(Type Libraries)和接口定义语言(Interface Definition Language)文件的超集。需要指出的是,CLR的元数据比它们还要完整。并且与类型库和IDL不同的是,元数据总是与包含IL代码的文件相关联。事实上,元数据总是和这些代码(IL代码)嵌入到同一个EXE/DLL文件中,两者不可分离。因为编译器同时产生元数据和IL代码并且将它们绑定到生成的托管模块中,元数据和IL代码总能保持同步。
元数据有很多用途,这里介绍一些:
- 元数据使得编译时不再需要头文件和库文件,因为所有被引用的类型和成员的信息都已经包含在实现类型和成员的IL文件中。编译器可以直接从托管模块中读取元数据。
- Visual Studio 使用元数据帮助编写代码。它的智能感知特性通过解析元数据告诉你一个类型所提供的方法、属性、事件和字段,以及方法中的参数。
- CLR的代码验证过程使用元数据来确保你的代码仅执行“安全”的操作。
- 元数据允许一个对象的字段序列化到内存块中,传递到另外一台计算机中,然后反序列化,并在远程的计算机上重新创建这个对象的状态。
- GC使用元数据跟踪对象的生命周期。对于任意一个对象,GC能从元数据获得其类型,以及哪个字段引用了其他对象。
在第二章中,“生成、打包、部署和管理应用程序及其类型”中,我将会在很多细节中描述元数据。
C#,VB,JScript,J#和IL编译器总是产生包含托管代码(IL)和托管数据(GC数据类型)的模块。最终用户必须在他们的机器上安装CLR(目前作为.NET Framework的一部分发布)以执行这些包含托管代码或者托管数据的模块。这和为了运行MFC或者VB6.0的应用程序而必须先安装MFC库或者VB DLLs是同一个道理。
默认情况下,C++编译器生成包含非托管(本地)代码和数据的EXE/DLL模块,在运行时操作的是非托管数据(本地内存)。这些模块并不需要CLR去执行。然而,如果指定/CLR命令行选项,C++编译器就会生成包含托管代码的模块,并且执行环境也必须安装CLR。在所有提到的Microsoft的编译器中,C++是唯一一个允许开发人员编写托管和非托管代码并将其生成在同一个模块中的编译器,它也是Microsoft唯一一个允许开发人员在他们的源代码中同时定义托管代码和非托管代码的编译器。
将托管模块组合到程序集
实际上,CLR并不是和模块一起工作,是和程序集一起工作的。程序集是一个抽象的概念,在开始接触的时候很难掌握它。首先,程序集是一个或多个模块或资源文件的逻辑组合(即程序集由多个模块和资源文件组成)。其次,程序集是复用、安全、版本的最小单元。根据你选择的编译器或工具,你可以生成一个单一文件或者多个文件的程序集。在CLR中,程序集就是我们所说的组件(Component)。
下图解释了什么是程序集。
在该图中,使用工具对托管模块和资源(数据)文件进行处理。这个工具生成一个单一的PE32(+)文件来代表文件的逻辑组合。这个PE32(+)文件包含一个叫做manifest的数据块。manifest是另一个元数据表的集合。这些表描述了构成这个程序集的文件、由这些文件实现的公有导出类型以及和这个程序集相关的资源或数据文件。
默认情况下,编译器会将生成的托管模块转换成程序集。也就是说,C#编译器生成一个包含manifest的托管模块,其中的manifest表明程序集仅包含一个文件。所以,对于一个仅包含托管模块并且没有资源(数据)文件的项目来说,这个程序集就是托管模块,你在创建过程中,并不需要任何附加的操作。如果你想要把一组文件组合到一个程序集中,就需要使用更多的工具(assembly linker,AL.exe)及其命令行选项。我将在第二章中解释这些内容。
对于一个可重用、可部署(securable)、可实施版本管理(versionable)的组件来说,程序集可以将逻辑表示和物理表示相分离(解耦)。如何将代码和资源放置于不同的文件则完全取决于我们自己。
一个程序集模块同时包含其他被引用的程序集的信息(包括他们版本号)。这个信息使得程序集可以“自己描述”。换句话说,为了执行程序集中的代码,CLR可以确定程序集的直接依赖。不需要再在注册表或活动目录领域服务(Active Directory Domain Services)中获取更多的信息(DLL无需在注册表中注册信息)。因为不需要其他信息,部署程序集比部署非托管组件更简便。
加载CLR
HKEY_LOCAL_MACHINE\SOFAWARE\Microsoft\.NETFramework\policy
从.NET Framework2.0版本开始,SDK自带的命令行工具CLRVer.exe可以显示安装的所有CLR的版本。

在windows检查EXE文件头部以决定是创建32位,64位还是WoW64进程后,windows将x86,x64或者IA64版本的MSCorEE.dll加载到进程的地址空间中。在x86版本的Windows中,x86版本的MSCorEE.dll位于C:\Windows\System32目录下。x64或IA64版本的Windows中,x86版本的MSCorEE.dll位于C:\Windows\SysWow64目录,64位版本的位于C:\Windows\System32目录(为了向后兼容)。然后,进程的主线程调用MSCorEE.dll中定义的方法。该方法初始化CLR,加载EXE程序集,然后调用它的入口点方法(Main)。这样托管的应用程序就建立并运行了。(可以在代码中使用Environment的Is64BitOperatingSystem或Is64BitProcess属性来检查是否运行在64位版本的Windows或运行在64位的地址空间中)。执行程序集代码
在Main方法执行前,CLR检查所有被Main方法引用的类型。这将导致CLR分配一个内部的数据结构,这个数据结构用来管理这些被引用的类型。在上图中,Main方法只引用了一个类型,Console,所以CLR分配了一个单一的内部数据机构。这个内部数据结构包含在Console类型中定义的每个方法的入口。每个入口都有一个地址,通过这个地址可以找到方法的实现部分。当初始化这个结构时,CLR把每个入口设置成内部的一个没有正式记录的函数,我们暂且成该阐述为JITCompiler。
这样,一个方法只有在被首次调用时才会产生一些性能损失。所有对该方法后续的调用都将以本地代码做全速执行,因为本地代码不再需要验证和编译。
当生成非优化代码时,C#编译器会在代码中生成NOP(no-operation,非操作)指令。NOP指令可以在调试的时候使用“编辑后继续运行(edit-and-continue)”的特性,同时也允许在控制流指令(如for、while、do、if、else、try、catch和finally)中设置断点,以易于调试。在生成优化的IL代码时,C#编译器会移除这些NOP指令。IL与验证
不安全的代码
本地代码生成器NGen.exe
- 缩短应用程序的启动时间 运行NGen.exe可以缩短启动时间,因为代码已经被编译成本地CPU指令,在运行的时候不需要再编译。
- 减少应用程序的工作区大小 你可能知道一个程序集可以同时被多个进程/应用程序域加载,在这个程序集中使用NGen.exe能够减少应用程序的工作区。因为NGen.exe可以将IL编译成本地CPU指令,并将输出保存到一个独立的文件中。这个文件可以通过内存映射到多进程的地址空间,并允许代码共享;每个进程不需要单独的拷贝。
- 没有代码保护 许多人可能觉得可以不用发布IL代码文件而只发布NGen后的文件,这样就可以保护他们的代码。不幸的是,这是不可能的,在程序运行的时候,CLR需要访问程序集的元数据;这就需要发布包含IL和元数据的程序集。另外,如果出于一些原因,CLR不能使用NGen.exe编译后的文件,那么CLR就需要JIT编译器编译程序集的IL代码,这是IL就必须是可用的。
- NGen的文件可能会不同步 当CLR加载NGen的文件时,会比较前期编译的代码和当前执行环境的诸多特征。如果其中有一个特征不符合,NGen的文件就不能使用,取而代之的仍然是普通的JIT编译过程。必须匹配的特征如下:
- 执行时性能差 编译代码的时候NGen不能像JIT编译器那样对执行环境做很多假设,这也导致NGen 生产低效的代码。
框架类库
- Web services 这个方法可以使得将基于XML的信息放送到Internet的操作变得很容易。
- Web Froms 基于HTML的应用程序(Web sites)。通常情况下,Web窗体应用程序一些数据库查询和web服务调用,并对返回的信息进行组合和筛选,最后使用一个基于rich-HTML的用户界面将信息表示在浏览器中。
- Windows Forms 富windows GUI应用程序。你可以使用windows桌面提供的更强大的,更高级的函数创建你的应用程序的UI来代替Web窗体。Windows窗体可以使用控件、菜单、鼠标和键盘事件,甚至可以直接和底层操作系统交互信息。和Web窗体应用程序一样,Windows窗体一样也可以进行数据库查询和调用Web services。
- Windows console applications 一个对UI要求不高的应用程序,控制台应用程序提供一种快速、容易的方法来创建应用程序。各种编译器、实用程序、工具通常被实现为控制台应用程序。
- Windows services 利用.NET Framework,我们可以生成由Windows服务控制管理器(Windows Service Control Manager,SCM)控制的服务程序。
- Component library .NET Framework允许你生成独立的程序集(组件),它们可以应用于前面提到的各种应用程序。
这本书是关于CLR和一些与CLR紧密关联的常规类型。所以这本书里面的内容可以应用于所有面向CLR的应用程序和组件。很多其他的好书会讲解一些特定的应用程序,比如web服务,web窗体,windows窗体等等。这些特定应用程序的书会帮助大家从高层的角度来学习.NET应用程序开发,因为他们关注的是应用程序的种类,而非底层开发平台。在这本书里,我会教你许多底层的东西,读完本书之后,大家可以再读一些特定的应用程序相关的书籍,便可以轻松熟练的创建各种.NET框架应用程序了。通用类型系统
- Field 数据变量,对象状态的一部分。字段通常由它们的名字和类型来识别的。
- Method 在对象上执行某种操作的函数,可以改变对象的状态。方法有一个名字、签名和修饰符。签名指定参数的数量、系列和类型,是否有返回值以及返回值的类型等等
- Property 对于调用者,属性就像是一个字段。但对于属性的实现者,属性看起来就像是一个方法。属性允许实现者在访问数值之前验证输入参数和对象状态,或者在必要的时候只计算数值。属性的调用者只需要很简单的句子就可以完成属性的调用。最后属性允许你创建只读或只写“字段”。
- Event 事件允许在一个对象和其他相关联的对象之间建立一个通知机制。例如,一个按钮可以提供一个事件,当按钮被按下时,将通知其他对象。
- Private 方法只能被同一个类型中的其他方法所调用。
- Family 方法可以被子类调用,不管他们是不是在同一个程序集。C++和C#用protected代替family。
- Family和assembly 方法可以被子类调用,但是子类只能在同一个程序集中实现。C#用protected internal代替family或者assembly。
- Public 方法可以被任何程序集的代码调用。
- 判断两个实例是否相等。(Equals)
- 获得实例的hash码。(GetHashCode)
- 查询实例的真正类型。(GetType)
- 实现实例的浅拷贝。()
- 获得实例当前状态的字符串表示。(ToString)
通用语言规范
在图中,CLR/CTS提供了一系列特性。一些语言提供了CLR/CTS的较大子集。举个例子,开发人员使用IL汇编语言可以使用所有CLR/CTS提供的特性。而大多数语言,如C#、VB、Fortran,提供了CLR/CTS特性的一个子集。CLS定义了所有语言必须支持的一个最小的特性集合。using System;
// Tell compiler to check for CLS compliance
[assembly: CLSCompliant(true)]
namespace SomeLibrary {
// Warnings appear because the class is public
public sealed class SomeLibraryType {
// Warning: Return type of 'SomeLibrary.SomeLibraryType.Abc()'
// is not CLS-compliant
public UInt32 Abc() { return 0; }
// Warning: Identifier 'SomeLibrary.SomeLibraryType.abc()'
// differing only in case is not CLS-compliant
public void abc() { }
// No warning: Method is private
private UInt32 ABC() { return 0; }
}
}
在这段代码中,我们在程序集上应用了[assembly:CLSCompliant(true)]特性。这个特性告诉编译器确保任何一个公共暴露的类型中不会有阻止其他编程语言访问该类型的构造。当代码编译时,C#编译器会发出两个警告。第一个警告是因为Abc方法返回一个无符号整数,有些编程语言不能操作无符号整数值。第二个警告是因为该类型暴露的两个公有方法abc与Abc仅存在大小写和返回值的差别,VB等语言不能调用这两个方法。
using System;
internal sealed class Test {
// Constructor
public Test() { }
// Finalizer
~Test() { }
// Operator overload
public static Boolean operator ==(Test t1, Test t2) {
return true;
}
public static Boolean operator !=(Test t1, Test t2) {
return false;
}
// An operator overload
public static Test operator +(Test t1, Test t2) { return null; }
// A property
public String AProperty {
get { return null; }
set { }
}
// An indexer
public String this[Int32 x] {
get { return null; }
set { }
}
// An event
public event EventHandler AnEvent;
}
当编译器编译这段代码的时候会得到一个类型,其中含有大量字段和方法。可以使用.NET框架SDK配套的IL反编译工具(ILDasm.exe)来检查最终生成的托管模块,如图

下表1-4 解释了编程语言构造是如何映射到CLR字段/方法
Test类型还有另一些节点未在表1-4中列出,其中包括.CLASS,.custom,AnEvent,AProperty以及Item——它们标识了与类型有关的其他元数据。这些节点不映射到字段或方法;它们只是提供了有关类型的一些额外的信息,供CLR、编程语言或者工具访问。例如,利用Reflector工具,可以发现Test 类型提供了一个名为AnEvent的事件,该事件通过两个方法(add_AnEvent 和 remove_AnEvent)公开。与非托管代码互操作
- 托管代码可以调用DLL中的非托管函数 托管代码可以用一种名为P/Invoke(Platform Invoke的简称)的机制来轻松调用DLL中包含的函数。总之,FCL中定义的许多类型都要在内部调用从Kernel32.dll、User32.dll等导出的函数。许多编程语言都提供了一个机制,允许托管代码方便地调用包含在DLL中的非托管函数。例如,C#应用程序可调用从Kernel32.dll 导出的CreateSemaphore函数。
- 托管代码可以使用现有的COM组件(服务器) 许多公司已经实现了许多非托管的COM组件。使用这些组件的类型库,可以创建一个托管程序集来描述COM组件。托管代码可以访问像访问其他任何托管类型一样访问托管这个程序集中的类型。有关这方面的信息,你可以.NET Framework SDK配套提供的TlbImp.exe工具。有的时候,你可能没有一个类型库,或者想对TlbImp.exe生成的内容进行更多的控制。在这种情况下,可以在源代码中手动构建一个类型,使CLR能用它来实现正确的互操作性。例如,可以从一个C#应用程序中使用DirectX COM组件。
- 非托管代码可以使用托管类型(服务器) 许多现有的非托管代码需要你提供一个COM组件以保证代码可以正确地工作。许多现有的非托管代码要求提供一个COM组件,从而确保代码正确工作。使用托管代码可以更简单地实现这些组件,避免所有代码都不得不和引用计数以及接口打交道。例如,可以使用C#来创建一个ActiveX控件或者一个shell扩展。这方面的详情可以参见.NET Framework SDK配套提供的TlbExp.exe和RegAsm.exe工具。

浙公网安备 33010602011771号