[CLR的执行模型].NET应用程序是如何执行的?
通常为了开发一个应用程序,我们首先要选择一个开发平台,然后,我们必须决定使用哪一种编程语言。通常这是一个艰难的抉择,因为,不同的语言有不同的本事,当然前提条件是作为开发着的你能应用这种语言。因为DebugLZQ这篇文章讨论的是在.NET Framework开发平台上进行开发,在此之上选择的语言只能是面向“公共语言运行时”的语言,其中包括:C++/CLI,C#,Visual Basic,JScript,J#和一个中间语言汇编器(Intermediate Language Assembler)。除了MS,一些公司,大学也推出面向运行时的代码产品,我知道的有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。既然如此,不同编程语言的优势何在?事实上,可以将编译器是为语法检查器和“正确代码”的分析器。 他们检查源代码,确定你写的一切都有意义,然后输出对你的意图进行描述的代码。不同的语言允许用不同的语法来开发。不要低估这个选择的价值。例如:对于数学或者金融类的应用程序,使用APL语言比使用perl语言可以多节省许多的开发时间。事实上,.NET应用程序在运行时,CLR根本不关心开发人员用的是哪一种语言,因为我们用具体语言写的源代码已经被相应的编译器编译为托管模块(managed module)。DebugLZQ要表达的意思,如下图(From Jeffrey Richter,DebugLZQ向大牛J·R致敬!!!):
上图显示了编译源代码文件的过程。从这个图上看出,你可以使用任何一种支持CLR语言编写源代码文件。然后相应的编译器将检查语法,并分析源代码。不管你使用何种编译器,结果都是一个托管模块。托管模块是一个标准的32位Microsoft Windows可移植执行文件(PE32),或者是一个标准64位Microsoft Windows可移植执行文件(PE32+)。
由编译器编译而来的这个叫“托管模块”具体是什么样子的呢?下面展示的是托管模块的哥哥组成部分(本来想用DebugLZQ自己的语言来说的,但是担心讲的不够准确,博友们你懂的,最担心遇到向DebugLZQ这样没水平的瞎扯,误人子弟啊,简直是坑爹!!所以DebugLZQ在重要的引用之处尽量保持大牛们原来的描述的样子,不是DebugLZQ投机取巧,实在是不想误导各位博友!):
需要说明的是:与本地代码编译器不同, 本地代码编译器生成的是面向特定CPU架构的代码。 而每个面向CLR 的编译器生成都是IL(中间语言)代码。
除了生成IL,面向CLR的每个编译器还要在每个托管模块中生成完整的元数据。简单地说,元数据(metadata)是一组数据表。其中一些数据表描述了模块中定义的内容,比如类型及其成员。还有一些元数据描述了托管模块引用的内容,比如导入的类型及其成员。由于编译器同时生成元数据和IL,把他们绑定在一起,并嵌入最终生成的托管模块,所以,元数据和它描述的IL代码永远不会失去同步。
MS的 C# ,Visual Basic ,F# 和 IL 汇编器总是生成包含托管代码( IL )和托管数据(垃圾收集的数据类型)的模块。 为了执行包含托管代码以及 /或者托管数据的模块,最终用户必须在自己的计算机上安装 好CLR(目前作为.NET Framework的一部分提供)。这类似于为了运行Visual Basic 6应用程序,用户必须安装 Microsoft Foundation Class(MFC MFC)库或者Visual Basic DLLs 。
MS的C++编译器默认生成包含非托管(本地)代码的EXE/DLL模块,并在运行时操纵非托管数据(本地内存)。这些模块不需要CLR即可执行。然而,制定一个/CLR命令行开关,C++编译器就能生成包含非托管代码的模块。当然,最终用户必须安装CLR执行这种代码(很神奇吧!!!)。
由编译器生成托管模块后,是否程序可以运行了呢?答案是否定的。CLR实际不和模块一起工作。相反,它是和程序集一起工作的。程序集(assembly)是一个抽象的概念。首先,程序集是一个或者多个模块/资源文件的逻辑性分组。其次,程序集是重用、安全性以及版本控制的最小单元。取决于你对编译器或工具的选择,既可以生成单文件的程序集,也可以生成多文件的程序集。在CLR的世界中,程序集详单与一个“组件”。
默认情况下,编译器实际会把生成的托管模块转换成程序集。用图形表示如下:
你生成的每个程序集既可以是一个可执行的应用程序,也可以是一个DLL(其中含有一组由可执行程序使用的类型)。当然,最终是有CLR管理这些程序集中代码的执行。
在介绍CLR具体如何加载之前,需要来讨论Windows的32位和64位版本。实际上,由编译器生成的EXE文件不仅能在32位Windows上运行,还能在64位的Windows的x64和IA64版本上运行!极少数情况下,开发人员向希望代码智能在一个特定版本的windows上运行。为了帮助这些开发人员,c#编译器提供了一个/platform命令行开关选项。这个开关允许开发人员选择运行的目标平台。如果不具体指定一个平台,默认选项是anycpu,表明最终要生成的程序集能在任何版本的windows上运行。如下图:
运行一个可执行文件时,Windows会检查这个EXE文件的头,判断应用程序需要的是32位地址空间,还是64位地址空间。下图总结了C#编译器却ing不同的/platform命令行开关时,会获得哪一种托管模块。其次,它总结了应用程序在不同版本的windows上如何运行。
Windows检查好EXE文件头,决定是创建32位,64位还是WoW64进程之后,会在进程的地址空间中加载MSCorEE.dll的x86,x64或IA64版本。
然后,进程主线程调用MSCorEE.dll中定义的一个方法。将这个方法初始化CLR,加载EXE程序集,然后调用其入口方法(Main)。随即,托管的应用程序将启动并运行。
然后,执行程序集的代码。为了执行一个方法,首先必须将它的IL转换成本地CPU指令。这是CLR的JIT(just-in-time)编译器的职责。下图展示了一个方法首次调用时发生的事情。
在Main方法执行前,CLR检查所有被Main方法引用的类型。这将导致CLR分配一个内部的数据结构,这个数据结构用来管理这些被引用的类型。在上图中,Main方法只引用了一个类型,Console,所以CLR分配了一个单一的内部数据机构。这个内部数据结构包含在Console类型中定义的每个方法的入口。每个入口都有一个地址,通过这个地址可以找到方法的实现部分。当初始化这个结构时,CLR把每个入口设置成内部的一个没有正式记录的函数,我们暂且成该阐述为JITCompiler。
这样,一个方法只有在被首次调用时才会产生一些性能损失。所有对该方法后续的调用都将以本地代码做全速执行,因为本地代码不再需要验证和编译。