• [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

      (1)当Main第一次调用WriteLine时,JITCompiler函数也被调用。JITCompiler负责吧一个方法的IL代码编译成本地CPU指令。因为IL是被“即时(just in time)”编译的,所以CLR的这一部分通常被称作JITter或者JIT编译器。
      当JITCompiler函数被调用时,它知道哪个方法被调用,在这个方法中定义了什么类型。(2)JITCompiler函数在程序集的元数据中查找被调方法的IL代码。JITCompiler验证IL代码并将其编译编译成本地CPU指令。本地CPU指令保存在一个动态分配的内存块中。然后(3)JITCompiler将前面内部数据结构中被调用方法的地址替换成包含本地CPU指令的内存块地址(4)最后JITCompiler函数会跳转到内存块中的代码。这里的代码就是WriteLine方法的实现(含有一个String类型参数的版本)。(5)当这些代码执行完,它将返回到Main函数中,Main函数会接着继续执行下面的代码
          现在Main第二次调用WriteLine方法。这一次,WriteLine的代码已经经过验证和编译。所以这次将直接调用到内存快,完全跳过了JITCompile函数。WriteLine执行完后,同样返回到Main
      下图说明了第二次调用WriteLine方法时的过程。

          这样,一个方法只有在被首次调用时才会产生一些性能损失。所有对该方法后续的调用都将以本地代码做全速执行,因为本地代码不再需要验证和编译。

      JIT编译器将本地存储在动态内存之中。这以为这当应用程序关闭时,编译生成的本地代码将被丢弃。这样,如果我们以后再次运行该应用程序,或者同时运行该应用程序的两个不同实例,JIT编译器需要再次将同样的IL代码编译成本地指令
      对于大多数的应用程序,JIT编译引起的损失是微不足道的。而且,大部分的也能够用程序经常反复调用同一个方法。这样,在应用程序执行时,这些方法引起的也只是一次性能损失。而且通常方法内部执行所花费的时间要比方法调用本身索花费的时间要多的多。
          还需要注意的是,CLR的JIT编译器会对本地代码进行优化。同样地,可能要花费很多的时间来完成优化的代码,但是,和没有优化的代码相比,代码在优化之后将获得更出色的性能。事实上在第二个变异阶段MS进行了大量的优化工作,将额外的开销保持在最低。
          虽然这样说很难让人信服,单很多人(包括我)都认为托管应用程序的性能实际上超过了非托管应用程序。有很多原因是我们对此深信不疑。下面引用JR给出的理由:

          如果试验表明, CLR 的 JIT 编译器似乎没有使自己的应用程序达到性能,还应该使用 .NET Framework SDK.配套提供的NGen.exe工具。这个将一程序集的所有代码编译成本 ,并将这些本保存到一个磁盘文件中。在运行时,  一旦加载程序集CLR CLR就会自动判断是否存在该程序集的一个预编译版本。如果, 是否存在该程序集的一个预编译版本。如果是CLR就加载预编译代码。这样一来,避免了在运行时进 就加载预编译代码。这样一来,避免了在运行时进 就加载预编译代码。这样一来,避免了在运行时进 就加载预编译代码。这样一来,避免了在运行时进 就加载预编译代码。这样一来,避免了在运行时进 行编译。注意, NGen.exe对最终执行环境做出的假设是非常保守(它不得如此)。所以,NGen.生 成的代码不会像JIT 编译器生成的代码那样进行高度优化。
          水平一般,能力有限,请各位博友见谅!!!!!!!!!!!!!!!!!
          [请点击下面绿色通道:关注DebugLZQ,与Debug一同学习,进步!]
  • 相关阅读:
    MUI(5)
    MUI(4)
    MUI(3)
    如何查找僵尸进程并Kill之,杀不掉的要查看父进程并杀之
    Linux下查看文件和文件夹大小
    CentOS,Ubuntu,Gentoo,Freebsd,RedHat,Debian的区别及选择
    eclipse4.3 安装tomcat8
    maven 代理
    ldap基本命令
    ssh-keygen
  • 原文地址:https://www.cnblogs.com/DebugLZQ/p/2287884.html
Copyright © 2020-2023  润新知