1.1将源代码编译成托管代码模块
如上图,
- 用支持CLR的任何一种语言来创建源代码文件。
- 再用一个对应的编译器来检查语法和分析源代码。
- 经编译器编译后生成托管模块(managed module),它是一个可移植执行体文件,它可能是32位(PE32)文件,也可能是64位(PE32+)文件。托管模块包括中间语言和元数据,需要CLR才能执行。
公共语言运行时(Common Language Runtime, CLR)是一个供多种编程语言使用的运行时。可用任何编程语言来开发代码,只要编译器是面向CLR的就行。
CLR也可以看作是一个在执行时管理代码的代理,管理代码是CLR的基本原则。能够被管理的代码称为托管(managed)代码,反之称为非托管代码。
托管模块是一个需要CLR环境才能执行的标准windows PE文件,包含IL和元数据以及PE表头和CLR表头:
- IL代码:就是中间语言。编译器编译源代码时生成的中间代码,在执行环境中,这些IL代码将被CLR的JIT编辑器翻译成CPU能识别的指令,供CPU执行。
- 元数据:实际上是一个数据表集合,用来描述托管模块中所定义和引用的内容。S能够智能感知就依赖元数据的描述。
- PE32/PE32+ 头:标准的 Windows PE文件头。包含文件类型,以及文件创建时间等信息。如果文件头使用PE32格式,则此文件只能在Windows 的32位或64位版本上运行;如果文件头使用PE32+格式,则此文件只能在Windows 的64位版本上运行。
- CLR表头:包含标识托管模块的一些信息。如CLR版本号,托管模块入口点方法(main方法)以及MethodDef(方法定义)元数据等等。
PE (Portable Execute) 可移植执行体文件是微软Windows操作系统上的程序文件,EXE、DLL、OCX、SYS、COM都是PE文件。
对于一个比较小的Program.exe应用程序,PE头和元数据占据了文件相当大的一部分。当然随着应用程序规模的增大,它会重用它的大部分类型以及对其他类型程序集的引用,造成元数据和头信息在整个文件中所占的比例逐渐减小。
本地代码编译器(natie code compiler)生成的是面向特定CPU架构(比如x86,x64)的代码。相反,每个面向CLR的编译器生成的都是IL代码。
中间语言IL(Intermediate Language)代码:编译器编译源代码后生成的代码(.exe或.dll文件),但此时编译出来的程序代码并不是CPU能直接执行的机器代码。在运行时,CLR的JIT编辑器将IL代码编译成本地CPU指令。
CPU:*处理器(Central Processing Unit),是一台计算机运算核心和控制核心。它的功能主要是解释计算机指令以及处理计算机软件中的数据。
DLL (Dynamic Link Library) 文件为动态链接库文件,是一种作为共享函数库的可执行文件。在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。多个应用程序也可以同时访问内存中单个 DLL 副本的内容。DLL 有助于共享数据和资源。
使用中间语言IL的优点(跨平台,跨语言):
- 可以实现平台无关性,即与特定CPU无关。
- 只需把.NET框架中的某种语言编译成IL代码,就实现.NET框架中语言之间的交互操作。
元数据(Metadata)是描述类型信息的数据,是由一组数据表构成的一个二进制数据块。元数据被CLR编译器编译后保存在Windows可移植执行体(PE)文件中,即和它描述的IL嵌入在EXE/DLL文件中,使IL和元数据永远同步。
元数据主要的类型表:
- 定义表(definition table):描述当前程序集中定义的类型和成员信息。
- 引用表(reference table):描述任何一个被内部类型引用的外部的类型和成员信息。
- 清单表(manifest table):包含了组成程序集所需要的所有信息,同时包含了对其他程序集的引用信息。
元数据的用途:
- 编译时,元数据消除了对本地C/C++头和库文件的需求,因为在负责实现类型/成员的IL代码文件中,已包含和引用的类型/成员有关的全部信息。编译器可以直接从托管模块读取元数据。
- CLR的代码验证过程中使用元数据确保代码只执行“类型安全”的操作。
- 使用元数据帮助您写代码。它的“智能感知”(IntelliSense)技术可以解析元数据,指出一个类型提供了哪些方法、属性、事件和字段。
元数据允许将一个对象的字段序列化到一个内存中,将其发送给另一台机器。然后反序列化,在远程机器上重建对象的状态。(内存:与CPU进行沟通的桥梁,计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大)。
元数据允许垃圾回收器跟踪对象的生存期,垃圾回收器能判断任何对象的类型,并从元数据知道那个对象中哪些字段引用了其他对象。
1.2 将托管模块合并成程序集
程序集(assembly)是一个或多个托管模块,以及一些资源文件的逻辑组合。是重用、安全性以及版本控制的最小单元。
CLR不和托管模块一起工作,是和程序集一起工作的。CLR是通过程序集与托管模块进行沟通的。
C#编译器将生成的多个托管模块和资源文件合并成程序集。在程序集内有一个清单,其描述了程序集内的文件列表,如托管模块、jpeg文件、XML文件等。
对于一个可重用的、可保护的、可版本控制的组件,程序集把它的逻辑表示(代码)和物理表示(资源)区分开。
1.3加载公共语言运行时
Microsoft创建了重分发包(redistribution package),允许将.NET Framework免费分发并安装到你的用户的计算机上。检查机器上是否安装好.Net Framework,只需在C:\Windows\System32下检查是否有MSCorEE.dll文件(Microsoft .NET Runtime Execution Engine/运行时执行引擎)。MSCorEE.dll一定是唯一的,且总是处于系统目录的system32下。MSCorEE.dll负责选择.NET版本、调用和初始化CLR等工作。Windows键+R \ regedit \ KEY_LOCAL_MACHINE \ SOFTWARE \ MICROSOFT NET Framework了解安装了哪些版本的.NET Framework。
X86指的是一种CPU的架构,是硬件。因为intel的8086,286,386~586而得名,amd开发的大部分CPU也是基于x86架构的。x86架构的特点是CPU的寄存器是32位(32-bit)的,因此也叫32位CPU。基于32位CPU开发的操作系统就叫32位操作系统。(寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。)
C#编译器生成的程序集要么包含一个PE32头,要么包含一个PE32+的头。
加载CLR的过程:
- 当双击一个.exe文件时,Windows会检查EXE文件的头(PE32头或PE32+头),判断应用程序需要的是32位地址空间,还是64位地址空间。
- 会在进程的地址空间中加载MSCorEE.dll的x86,x64版本。
- 进程的主程序调用MSCorEE.dll中定义的_CorExeMain方法,这个方法初始化CLR(具体如下),加载EXE程序集,然后调用其入口方法(Main)。
- 托管的应用程序将启动并运行。
初始化CLR包括:
- 分配一块内存空间,建立托管堆及其它必要的堆,由GC监控整个托管堆。
- 创建线程池(一种多线程处理形式)。
- 创建应用程序域 (AppDomain)。
1.4执行程序集的代码
- 在Main方法执行之前,CLR会检测出Main的代码中引用的所有类型,此处为Console类型。这导致CLR为该类型创建一个内部数据结构,它用于管理对所引用的类型的访问。
- 在这个内部数据结构中,Console类型定义的每个方法都有一个对应的记录项。对这个内部数据结构进行初始化时,CLR将每个记录项都指向包含在CLR内部的一个未文档化的函数(C#中没有函数的概念,一律称为方法)。这个函数称为JITCompiler。JITCompiler函数负责将一个方法的IL代码即时编译成本地CPU指令。
- Main方法首先调用WriteLine方法时,JITCompiler函数会被调用。它知道要调用的是哪个方法,以及具体是什么类型定义了该方法。
- 然后,JITCompiler会在定义该类型的程序集的元数据中查找被描述的方法的IL。
- 接着,JITCompiler验证IL代码,并将IL代码即时编译成本地CPU指令。
- 本地CPU指令被保存到一个动态分配的内存块中。
- 然后,JITCompiler返回CLR为类型创建的内部数据结构,找到与被调用的方法对应的那一条记录项,修改最初对JITCompiler的引用,让记录项现在指向内存块的地址。
- 最后,JITCompiler函数跳转到内存块中的代码。这些代码正是WriteLine方法的具体实现。这些代码执行完毕并返还时,会返还到Main中的代码,并向往常一样继续执行。
现在,Main要第二次调用WriteLine。这一次由于已对WriteLine的代码进行了验证和编译,所以会直接执行内存块中的代码,完全跳过JITCompiler函数。
一个方法只有在首次调用时才会造成一些性能损失。以后对该方法的所有调用都以本地代码的形式全速进行,无需重新验证IL并把它编译成本地代码。
JIT编译器将本地CPU指令存储到动态内存中,一旦应用程序终止,编译好的代码也会被丢弃。所以,如果将来再次运行应用程序,JIT编译器必须再次将IL编译成本地CPU指令。
在Visual Studio中新建一个C#项目时,项目的debug配置指定的是/optimize-和/debug: full开关。Release 配置指定的是/optimize-和/debug: pdbonly开关。只有在指定/debug(+/full/pdbonly)开关的前提下,编译器才会生成一个Program Debug Database (PDB)文件。PDB文件帮助调试器查找局部变量并将IL指令映射到源代码。
IL和验证
堆和栈都是一种数据项按序排列的数据结构,都在进程的虚拟内存中,只能在一端(称为栈顶)对数据项进行插入和删除。
要点:
- 堆(heap),队列优先,先进先出 (first in first out)。
- 栈(stack),先进后出 (first in last out)。
IL是基于栈的。这意味着它的所有指令都要将操作数压入(push)一个执行栈,并从栈弹出(pop)结果。
操作数是操作符作用于的实体,是表达式中的一个组成部分,它规定了指令中进行数字运算的量 。操作数就是你直接处理的数据,操作数地址就是操作数存放在内存的物理地址。表达式是操作数与操作符的组合。通常一条指令均包含操作符和操作数。
IL的最大优势并不在它对底层CPU的抽象,而在于应用程序的健壮性和安全性。将IL编译成本地代码CPU指令时,CLR会执行一个名为验证(erification)的过程。这个过程会检查高级IL代码,确定代码所做的一切都是安全的。列如,验证会核实调用的每个方法都有正确数量的参数,传给每个方法的每个参数都具有正确的类型,每个方法的返回值都得到了正确的使用,每个方法都有一个返回语句,等等。
在windows中,每个进程都有它自己的虚拟地址空间,这是因为不能简单的信任一个应用程序代码。通过验证托管代码,可确保代码不会不正确地访问内存,不会干扰到另一个应用程序的代码。这样一来就可以放心的将多个托管应用程序放到一个Windows虚拟地址空间中运行。
事实上,CLR确实提供了在一个操作系统进程中执行多个托管应用程序的能力。每个托管的应用程序都在一个APPDomain中执行。默认情况下,每个托管的EXE文件都在它自己的独立地址空间中运行,这个空间地址只有一个APPDomain。然而,CLR的宿主进程(比如IIS,SQL Serer)可决定在单个操作系统进程中运行多个APPDomain。
1.5本地代码生成器:NGen.exe
1.6 Framework类库
.Net Framework中包含了Framework类库(Framework Class Library, FCL)。FCL是一组DLL程序集的统称,其中包含了数千个类型定义,每个类型都公开了一些功能。
由于FCL包含数量极多的类型,所以有必要将相关的一组类型放到一个单独的命名空间中。
System命名空间包含Object基类型,其他所有类型最终都是从预定义的System.Object类型继承,Object是其他所有类型的根。,包含了用于整数、字符、字符串、异常处理以及控制台I/O的类型。
System.Object类型允许做下面事情:
- 比较两个类型的相等性 public static bool Equals(object objA, object objB); / public static bool ReferenceEquals(object objA, object objB);
- 获取实例的哈希码 public virtual int GetHashCode();
- 查询一个实例的真正类型 public Type GetType();
- 执行实例的浅拷贝 protected object MemberwiseClone();//创建一个当前object对象的浅表副本
- 获取实例对象的当前状态的一个字符串的表示 public virtual string ToString();
为了使用Framework的任何一个功能,必须知道这个功能是由什么类型提供的,以及该类型包含在哪个命名空间中。
部分常规FCL命名空间:
- System.Collections.Generic 命名空间包含定义泛型集合的接口和类,泛型集合允许用户创建强类型集合,它能提供比非泛型强类型集合更好的类型安全性和性能。
- System.Web 命名空间包含了所有网站开发相关的命名空间和类。
- System.Data 命名空间包含了提供数据访问功能的命名空间和类。
- System.IO 命名空间包含了数据流读写相关功能的类。
- System.Configuration 命名空间提供以变成方式访问.NET空间配置和处理配置文件(.config文件)中的错误的类和接口。
- System.DateTime 命名空间表示时间上的一刻,通常以日期和当天的时间表示。
- System.Linq 命名空间提供支持使用语言集成查询 (LINQ) 进行查询的类和接口。
- System.Globalization 命名空间包含定义区域性相关信息的类,这些信息包括语言、国家/地区、使用的日历、日期、货币和数字的格式模式以及字符串的排序顺序。
1.7通用数据类型
CLR是完全围绕类型展开的。由于类型是CLR的根本,所以Microsoft制定了一个正式的规范,即“通用数据类型”(Common Type System),它描述了类型的定义和行为。
CTS规范规定一个类型可以零个或多个成员:
- 字段(Field)一个数据变量,是对象状态的一部分。字段根据名称和类型来区分。
- 方法(Method)一个函数,能针对对象执行一个操作,通常会改变对象的状态。方法有一个名称,一个签名以及一个或多个修饰符。签名指定参数的数量(及其顺序),参数的类型。方法如果有返回值,还要指定返回值的类型。
- 属性(Property)getter和setter
- 事件(Eent)事件在对象以及其他相关对象之间实现了一个通知机制。例如,利用按钮提供的一个事件,可以在按钮被点击之后通知其他对象。
CTS指定了类型可视性规则以及类型成员的访问规则:
- public同一程序集中的任何其他代码或引用该程序集的其他程序集都可以访问该类型或成员
- private只有同一类或结构中的代码可以访问该类型或成员
- protected只有同一类或结构或者此类的派生类中的代码才可以访问的类型或成员
- internal同一程序集中的任何代码都可以访问该类型或成员,但其他程序集中的代码不可以
1.8公共语言规范
CLR集成了所有语言,因为CLR使用了标准类型集、元数据以及公共执行环境,所以允许在一种语言中使用由另一种语言创建的对象。
Microsoft定义了一个公共语言规范CLS(Common Language Specification),它详细定义了一个最小功能集。