CLR via C#学习笔记(1)
.NET
的CLR
和.NET Core
的CoreCLR
主体没有太大区别,之前对于这本书的了解程度是在需要的时候去查阅,现在希望能够对其进行一个相对系统,整体的学习
CLR简介
CLR
,全名Common Language Runtime ,一般翻译成公共语言运行时
,所谓运行时,可以对标Java
生态中的JVM
,无论是什么编程语言,只要能通过各种编译器编译成托管模块(managed module),就可以通过CLR
执行,CLR
为其运行提供了环境,其核心功能包括:内存管理、程序集加载、安全性、异常处理和线程管理等
托管模块
托管模块是指通过面向CLR
的编译器编译的,最后通过CLR
运行的PE(Portable Executable:可移植执行体)文件
托管模块包括几个部分:
PE32或PE32+头,这里标识了托管模块可以运行的操作系统版本,以及文件类型(
GUI
,CUI
或DLL
),文件生成时间CLR头:包含要求的
CLR
版本,一些flags
,托管模块的入口函数(Main函数)的MethodDef
元数据token,以及包括模块的元数据、资源、强名称、标志,还有其他一些不太重要的数据项的位置/大小元数据(metadata):述源代码中定义的类型和成员,以及引用的类型和成员
IL(中间语言)代码:编译器编译源码产生的代码,在运行时,会被
CLR
编译成编辑CPU指令
程序集
CLR
直接与程序集(assembly)打交道,程序集是一个抽象概念,程序集中包含一个名为清单(manifest)的数据块。清单也是元数据表的集合,这些表描述了构成程序集的文件、程序集中文件所实现的公开导出的类型,与程序集关联的资源或数据文件等。
编译器默认将生成的托管模块转换成程序集,也就是说,C#编译器生成的是含有清单的托管模块。所以,对于只有一个托管模块且无其他资源文件的项目,程序集就是托管模块,生成过程中无需执行任何其他操作的步骤,但是如果希望将一组文件合并到程序集中,就需要程序集链接器和其他命令行选项。
在程序集的模块中,还包含与引用的程序集有关的信息(包括版本号)。这些信息使程序集可以自描述(self-describing),CLR
可以通过这些信息判断程序集的直接依赖对象(immediate dependency)是什么,而不需要在注册表或其他地方保存额外的信息,所以和非托管组件相比,程序集更容易部署
加载CLR
Windows执行可执行文件时,先检查其文件头,判断需要32位还是64位地址空间。其中,如果操作系统是64位的,需要运行32位Windows应用程序的话,会通过Wow64(Windows on Windows64)技术运行32位Windows应用程序。
判断完成之后,其会在进程地址空间加载MSCorEE.dll
,接着,进程调用MSCorEE.dll
中定义的一个方法,这个方法初始化CLR
,加载EXE
程序集,调用其入口方法(Main
),随即,托管应用程序启动并运行。
值得注意的是,如果非托管程序调用LoadLibrary
加载托管程序集,Windows会自动加载并初始化CLR。因为此时进程以及你个启动并运行了,所以可能会限制程序集的可用性,例如,64位进程完全无法加载使用文件头为PE32
的托管程序集
执行程序集的代码
托管程序集同时包含元素据和IL
,IL
是与CPU无关的机器语言,它比大多数CPU机器语言都高级,可以将其视为一种面向对象的机器语言,其能够访问和操作对象类型,具有创建和初始化对象、调用对象上的虚方法以及直接操作数组元素的指令
高级语言通常只公开了CLR全部功能的一个子集。然而,IL
汇编语言允许开发人员访问CLR的全部功能。所以如果你选择的编程语言隐藏了你迫切需要的一个CLR功能,可以换用IL
汇编语言或者提供了所需功能的另一种编程语言来写那部分代码
为了执行方法,首先必须把方法的IL
转换成本机(native)CPU指令。这是CLR的JIT(just-in-time)编译器的职责。
书里举了一个调用Console.WriteLine
函数的例子
其在方法首次被调用时,验证并将IL
代码编译成本机CPU指令,本机CPU指令保存到动态分配的内存块中。之后,JITCompiler
回到CLR
为类型创建的内部数据结构,替换被调用方法对应的那条记录的引用,使其指向内存块(包含了刚才编译号的本机CPU指令)的地址。最后,JITCompiler
函数跳转到内存块中的代码,代码执行完毕并返回时,会回到Main
中的代码,并像往常一样继续执行
第二次调用WriteLine
时,因为已经对WriteLine
的代码进行了验证和编译,所以会直接执行内存块中的代码,完全跳过JITCompiler
函数。所以,方法仅在首次调用时才会有一些性能损失。以后对该方法的所有调用都以本机代码的形式全速运行,无需重新验证IL
并把它编译成本机代码
程序重新启动,或者同时动应用程序的两个实例,JIT
编译器都会再次将IL
编译成本机指令。相比之下,本机(native)应用程序的只读代码页可由应用程序正在运行的所有实例共享
/optimize
和/debug
这两个编译开关对编译生成的IL
代码会有影响,这些编译选项主要是会对程序的调试提供帮助
因为JIT编译器
对程序执行环境的认识比非托管编译器更深刻,所以有理由相信,托管应用程序有能力超越非托管应用程序的性能
IL和验证
IL
基于栈,并且是无类型(typeless)的。其是对底层CPU的抽象,并且由于将IL
编译成本机CPU指令时,CLR
会执行一个验证过程,这个过程会检查IL
代码,确认代码所作的一切都是安全的,所以其构建的应用程序具有健壮性和安全性。
上面提到的可以验证安全性的代码,被称为安全(safe)代码,Microsoft C#编译器也允许开发人员写不安全的(unsafe)代码。不安全的代码允许直接操作内存地址,并可操作这些地址处的字节。这类包含不安全代码的所有方法都需要用unsafe
关键字标记。除此之外,C#编译器要求使用/unsafe
编译器开关来编译源代码
本机代码生成器
使用.NET Framework
提供的NGen.exe
工具,可以在应用程序安装到用户的计算机上时,将IL
代码编译成本机代码,其作用是可以提交程序的启动速度以及减少程序运行时独自占用的内存(其将IL
编译成本机代码,并保存到单独的文件中。该文件可以通过内存映射
的方式,同时映射到多个进程地址空间中,使代码得到了共享,避免每个进程都需要一份单独的代码拷贝)
但是,需要注意的是NGen
生成的文件是没有知识产权保护的,因为在运行时CLR
要求访问程序集的元数据(用于反射,序列化等功能),所以要求发布包含IL
和元数据的程序集,另外,如果因为各种原因,NGen
生成的文件失去了同步,也必须要对程序集的IL
进行JIT
编译,所以IL
代码必须处于可用状态。由于编译代码时,NGen
无法像JIT
编译器那样对执行环境进行许多假定,所以这会造成其造成性能较差的代码
Framework类库
.NET Framework
包含Framework
类库(Framework Class Library, FCL
),这是一组DLL
程序集的统称,其中定义了数千个拥有各自功能的类
通用类型系统
CLR
的一切围绕类型展开,Microsoft制定了一个正式的规范来描述类型的定义和行为,这就是通用类型系统