菜鸟CLR VIA C#之旅(1):品味细节,CLR的执行模型
从菜鸟刚接触到.net时,菜鸟就知道CLR VIA C#是一本很牛的书,为什么?CSDN会告诉你——总会有人问“学.net什么书籍好?”,这个本没有标准答案的问题,菜鸟却从各种大牛一致的回答中找到了标准答案:C#入门经典—>C#高级编程—>CLR VIA C#,于是乎,对于大牛们的信任,这三本书都躺在菜鸟的床头。虽然菜鸟很菜,但菜鸟喜欢在CDSN、博客园、codeproject(菜鸟英语不堪忍睹,每次都还需要打开Google翻译)上闲逛,属于那种不厚道的看帖不回帖的一员,甚至过了相当长的一段时间都还没有注册,不是菜鸟不想回答,而是菜鸟水平实在不堪忍睹,怕误导人家,更怕关公面前耍大刀。菜鸟发现自己错了,为什么?你懂的!为此在大三暑假的时候,菜鸟在CSDN上走火入魔似的回答了别人一个月的问题,即时菜鸟不会的(其实大多都不会),菜鸟会去翻书、找度娘、搜谷姐,想尽办法帮别人回答,一个月后菜鸟的等级竟然有一颗星星了,当时激动的跑到灌水区散了下分,但之后菜鸟又回到了看帖不回帖的状态,或许是开学了,或许是DOTA去了,或许是没激情了,反正具体什么原因已记不清楚。
“纸上得来终觉浅,须知此事要躬行”,如果仅仅是看别人的文章或书籍,可能就在看的时候有感觉,合上之后就什么也不记得了,所以菜鸟决定将自己看到的或学到的好东西记录下来——不再为看不懂高深的技术文章而烦恼,而是拿起《CLR VIA C#》踏踏实实从头到底边阅读边写笔记。
在菜鸟上篇菜鸟CLR VIA C#之旅—开始旅行:千里之行始于足下完成后,得到了很多大牛们的支持,在此菜鸟深表感谢,更要谢谢那些提出意见的高手们,志志同学首先发了了菜鸟一个低级的错误,“将Hello world写成了Hello word”,当时冯同学的一句“啥时候出一片 Hello,excel! 啊?”我还疑惑不解,哎,太粗心了。对于 toEverybody的留言:“又要人在研究C#编译器产生的乱码呀//唉....”菜鸟不敢苟同,但从这个留言中可以看出,很多的园友都把目光和焦点注意在如何理解IL代码这个问题上。这真是个莫大的好消息,因为很明显大家的思路慢慢的从应用向底层发生着转变,技巧性的东西是一个方面的积累,底层的探索为也是必不可少的修炼(anytao)。
对于.NET程序员来说,IL代码意味着:
Ø 通用的语言基础是.NET运行的基础,当我们对程序运行的结果有异议的时候,如何透过本质看表面,需要我们从本质入手来探索,这时 IL是你必须知道的基础;
Ø 元数据和IL语言是CLR的基础,了解必要的中间语言是深入认识CLR的捷径;
Ø 大量的事例分析是以IL来揭密的,因此了解IL是读懂他人代码的必备基础,可以给自己更多收获。
很明显这些优越性足以诱惑我们花时间和精力涉猎其中。然而,了解了IL的好处,并不意味着我们应该过分的来关注IL,有人甚至可以洋洋洒洒的写一堆IL代码来实现一个简单Hello world程序,但是正如我们知道的那样,程序设计已经走过了几十年的发展,如果纯粹的陶醉在历史中,除了脑子不好,没有其他的解释。不然看见任何代码都以IL的角度来分析,又将走进另一个误区,我们的宗旨是追求但不过分。
好了,开始《CLR VIA C# 》第一章 “CLR的执行模型”的学习,了解下应用程序是如何执行的。如果能清楚的了解代码是如何运行的,菜鸟认为对于以后代码的调试和优化将起到关键作用。
1 将源代码编译成托管模块
只要编译器是面向CLR的任何语言,其创建的源代码,都会用一个对应的编译器来检查语法和分析源代码,最终结果都是生成一个托管模块(managed module)。
托管模块
托管模块是一个标准的32位Microsoft Windows可移植执行体(PE32)文件,或者是一个标准的64位Windows可移植执行体(PE32+)文件,它们都需要CLR 才能执行。顺便说一句,托管的程序集总是利用了Windows的数据执行保护(Data Execution Prevention,DEP)和地址空间布局随机化(Address Space Layout Randomization,ASLR);这两个功能旨在增强整个系统的安全性。托管模块包括以下几个部分:
1)PE32或PE32+头:PE32能在windows32位或64位机器上运行,PE32+只能在64位机器上运行。
2)CLR头:包含了需要的CLR版本,一些flag,托管模块入口方法(Main方法)以及各种信息。
3)IL(中间语言)代码:编译器编译源代码后生成的代码,在运行的时候,CLR再将IL编译为本地CPU指令。
4)元数据:每个托管模块都包含元数据表,有两种类型,一种类型的表描述源代码中定义的类型和成员,一种类型的表描述源代码中引用的类型和成员。
元数据的用途:
Ø 在编译时,元数据消除了对头和库文件的需求。
Ø VS的“智能感知功能”就是依靠元数据来解译一个类型提供了什么方法、属性、事件和字段。如果是一个方法将是传入的参数。
Ø IL的“安全”检查需要元数据。
Ø 元数据可以对一个对象的代码序列化,存入到内存,发生到另一台机器上。可以在另一台机器上进行反序列化来重建对象状态。
Ø 元数据可以运行垃圾收集器跟踪对象的生存期。
2 将托管模块合并成程序集
在这幅图中,一些托管模块和资源(或数据)文件准备交由一个工具处理。该工具生成单独一个PE32(+)文件来表示文件的逻辑性分组。实际发生的事情是,这个PE32(+)文件包含一个名为“清单”(manifest)的数据块。清单是由元数据表构成的另一种集合。这些表描述了构成程序集的文件,由程序集中的文件实现的公开导出的类型,以及与程序集关联在一起的资源或数据文件。
程序集的类型
既可以生成单文件程序集,也可以生成多文件程序集,取决于你对于编译器或工具的选择。
程序集清单中的内容
信息 |
说明 |
程序集名称 |
指定程序集名称的文本字符串。 |
版本号 |
主版本号和次版本号,以及修订号和内部版本号。 公共语言运行时使用这些编号来强制实施版本策略。 |
区域性 |
有关该程序集支持的区域性或语言的信息。 此信息只应用于将一个程序集指定为包含特定区域性或特定语言信息的附属程序集。 (具有区域性信息的程序集被自动假定为附属程序集。) |
强名称信息 |
如果已经为程序集提供了一个强名称,则为来自发行者的公钥。 |
程序集中所有文件的列表 |
在程序集中包含的每一文件的散列及文件名。 请注意,构成程序集的所有文件所在的目录必须是包含该程序集清单的文件所在的目录。 |
类型引用信息 |
运行时用来将类型引用映射到包含其声明和实现的文件的信息。 该信息用于从程序集导出的类型。 |
有关被引用程序集的信息 |
该程序集静态引用的其他程序集的列表。 如果依赖的程序集具有强名称,则每一引用均包括该依赖程序集的名称、程序集元数据(版本、区域性、操作系统等)和公钥。 |
3 加载公共语言运行时
每个程序集既可以是一个可执行应用程序,也可以是一个DLL,但最终都是由CLR管理这些程序集中代码的执行, 这意味着必须在目标机器 上安装好.NET Framerwork.
要知道是否已安装.Net Framework,只需检查%SystemRoot%System32目录中的MSCorEE.dll文件。存在该文件,表明.Net Framework已安 装。
.NET Framework SDK提供了一个名为CLRVer.exe的命令行实用程序,它能列出一台机器上安装的所有CLR版本,比如菜鸟电脑上:
4 执行程序集的代码
托管程序集同时包含元数据和IL,IL是与CPU无关的机器语言,为了执行一个方法,必须把它的IL转换成本地CPU指令。
方法的第一次调用:
Main方法首次调用WriteLine时:
Ø JITCompiler函数被调用,它知道要调用的是哪个方法,以及具体是什么类型定义了该方法
Ø JITCompiler会在定义(该类型的)程序集的元数据中查找被调用的方法的IL。
Ø JITCompiler验证IL代码,并将IL代码编译成本地CPU指令。本地CPU指令被保存到一个动态分配的内存块中。
Ø JITCompiler返回CLR为类型创建的内部数据结构,找到与被调用的方法对应的那一条记录,修改最初对JITCompiler的引用,让它现在指向内存块(其中包含了刚才编译好的本地CPU指令)的地址。
Ø JITCompiler函数跳转到内存块中的代码。这些代码正是WriteLine方法(获取单个String参数的那个版本)的具体实现。这些代码执行完毕并返回时,会返回Main中的代码,并跟往常一样继续执行。
一个方法只有在首次调用时才会有一些性能损失,以后对该方法的调用都以本地代码的形式全速运行,无需重新验证IL并把它编译成本地代码。
一旦应用程序终止,编译好的代码也会被丢弃,所以再次运行,JIT编译器必须再次将IL编译成本地指令。
最后,了解下通用类型系统(Common Type System,CTS)以及公共语言规范(Common Language Specificcation,CLS)
CLR是完全围绕类型展开的,而了解类型则有必要把焦点放在.NET类型体系的公共基础架构上,由于通用类型系统的存在,.NET平台下的各种语言可以无缝的集成,从MSDN的官方解释上我们可以看到:
通用类型系统定义了如何在运行库中声明、使用和管理类型,同时也是运行库支持跨语言集成的一个重要组成部分。通用类型系统执行以下功能:
-
建立一个支持跨语言集成、类型安全和高性能代码执行的框架。
-
提供一个支持完整实现多种编程语言的面向对象的模型。
-
定义各语言必须遵守的规则,有助于确保用不同语言编写的对象能够交互作用。
我们都知道CTS支持两种基本的类型:值类型和引用类型,至于他们之间的区别,这不是本节的主要内容,以后会详细剖析.
菜鸟从anytao那里学习到:
.NET技术可以以规范和实现两部分来划分,而我们经常强调和提起的.NET Framwork,主要包括公共语言运行时(Common Language Runtime, CLR)和.NET框架类库(Framework Class Library, FCL),其实是对.NET规范的实现。而另外一部分:规范,我们称之为公共语言架构(Common Language Infrastructure, CLI),主要包括通用类型系统(CTS),公共语言规范(Common Language Specification, CLS)和通用中间语言(Common Intermediate Language, CIL)。我们以图的形式来看看CTS在.NET技术阵营中的位置。
-
CLI,.NET技术规范,已经得到ECMA(欧洲计算机制造商协会)组织的批准实现了标注化。
-
CTS,上面已将,此不冗述。
-
CLS,定义了CTS的子集,开发基于CTS的编译器,则必须遵守CLS规则,由本文开头的图中就可以看出CLS是面向.NET的开发语言必须支持的最小集合。
-
CIL,是一种基于堆栈的语言,是任何.NET语言编译产生的中间代码,我们可以理解为IL就是CLR的汇编语言。IL定义了一套与处理器无关的虚拟指令集,与CLR/CTS的规则进行映射,执行IL都会翻译为本地机器语言来执行。常见的指令有:add, box, call, newobj, unbox。另外,IL很类似于Java世界里的字节码(Bytecode),当然也完全不是一回事,最主要的区别是IL是即时编译(Just in time, JIT)方式,而Bytecode是解释性编译,显然效率上更胜一踌。
-
.NET Framework,可以说是CLI在windows平台的实现,运行与windows平台之上。
-
CLR,.NET框架核心,也是本系列的核心。类似于Java世界的JVM,主要的功能是:管理代码执行,提供CTS和基础性服务。对CLR的探讨,将伴随着这个系列的成长来慢慢展开,在此就不多说了。
-
FCL,提供了一整套的标准类型,以命名空间组织成树状形式,树的根是System。对程序设计人员来说,学习和熟悉FCL是突破设计水平的必经之路,因为其中数以万计的类帮助我们完成了程序设计绝大部分的基础性工作,重要的是我们要知道如何去使用。
要和其他对象完全交互,而不管这些对象是以何种语言实现的,对象必须只向调用方公开那些它们必须与之互用的所有语言的通用功能。为此定义了公共语言规范 (CLS),它是许多应用程序所需的一套基本语言功能。CLS 规则定义了通用类型系统的子集,即所有适用于通用类型系统的规则都适用于 CLS,除非 CLS 中定义了更严格的规则。
题外话
不要过于专著于技术,这里的技术指工作中用于开发的技术。在几年之后,当你只有.NET可以和你的孩子分享的话,是不是太可悲了。在软件行业,技术虽然一定程度决定了薪水、决定了职位,但是我们的生活并不是100%是工作,有时间为技术而发愁,为何不综合提升自己的其它能力呢?并且随着职位的上升,往往沟通能力、经济知识以及文学艺术修养比技术显得更重要,这个时候叹息自己过于专著技术往往为时过晚。
l 学习靠自己,不要期望别人教你什么,学习要主动;
l 不管水平高低,不要看不起自己,也不能看不起别人,学习要心态好;
l 不能不思进取,也不用让自己为技术所累,给自己多一点技术之外的时间;
l 如果时间不充裕,优先考虑学习基础的内容,同时也可以多关注一些新的思想;
l 如果别人能从你这里学到知识的话,那么你自己也一定学到了知识,请坚持分享。