大章1:CLR基础

CLR是什么

  • Common Language Runtime 公共语言运行时,CLR和Java虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集),并保证应用和底层操作系统之间必要的分离。

再丢几个概念,除了编译器,另外3个会在下面讲PE文件会提到,因为这3个是托管PE文件的构成部分。

  • 「编译器」检查源代码,确定根据所使用语言语法所写的代码是有意义的,并输出实现其意图的代码。

  • 元数据,每个托管模块都包含元数据表,主要是2种表:描述源代码中定义的类型和成员;描述源代码引用的类型和成员。

  • IL,中间语言代码,编译器编译源代码时生成的代码。在运行时,CLR将IL编译成本机CPU指令。

  • PE32或PE32+头,决定程序是以32位还是64位运行的文件头,它和VS中的目标平台中选项(下面叫做**/platform开关选项**)相关联

    image-20211022161435339

CLR、程序集是怎么跑起来的

① 项目运行流程
  1. Windows检查EXE文件头,决定是创建32位还是64位进程之后,会在进程地址空间加载MSCorEE.dll的对应版本(比如x86版本在%SystemRoot%\System32目录中)。
  2. 然后,进程的主线程调用MSCorEE.dll中定义的一个方法,这个方法初始化CLR,加载EXE程序集,再调用其入口方法(Main)。
  3. 随即,托管应用程序启动并运行。
② 从代码文件到托管模块运行流程

源代码 -> 编译器(是面向进行时的编译器) -> 托管模块(是32或64位的Windows可移植执行体文件)

image-20211021173506928

IL代码

① IL是很高级的机器语言。

IL是与CPU无关的机器语言,它比大多数CPU机器语言都高级。

IL基于栈。这意味着它的所有指令都要将操作数压入(push)一个执行栈,并从栈中弹出(pop)结果。

  1. 它是面向对象的,它能访问和操作对象类型,并提供指令初始化对象、调用对象虚方法等。

  2. 由于C#、VB语言、F#等高级语言,最终都是被对应编译器转换为IL代码,所以比如可以将C#(擅长I/O)、vb.net的代码集成到一个解决方案中一起写,好处是能同时用各自的语言特性语言所使用的CLR功能模块。如下,CLS是规定的所有语言必须支持的最小功能集。

  3. 为了执行方法,首先必须把方法的IL转换成本机(native)CPU指令。这是CLR的JIT(just-in-time或者“即时”)编译器的职责。

    JIT编译器: 因为JITCompiler是即时编译的,所以被称为JIT just-in-time。

JITCompiler参与了什么?

主要是JITCompiler做的事,它会把第一次遇到的IL代码进行验证+编译,最后放进内存中。

方法仅在首次调用时才会有一些性能损失,以后对该方法的所有调用都以本机代码的形式全速运行。

注意第一次执行方法WriteLine时会比第二次执行WriteLIne所消耗的多:

image-20211022164732481

image-20211022170151293

③ IL的最大优势不是他对底层CPU的抽象,而是健壮性和安全性。

CLR在IL编译成本机CPU时,会进行一步**验证**工序。它会检查高级IL代码,确定代码所做的一切都是安全的(比如方法的入参是否正确)。
Windows的每个进程都有自己的虚拟地址空间,给进程自己的独立地址空间,是为了相互之间不影响。当然每个进程可以有多个AppDomain,此不多拓展。

④ unsafe代码

c#编译器默认生成safe代码,这种代码安全性可以验证。然而c#编译器也允许unsafe代码存在,这类代码允许直接操作内存地址,并可操作这些地址处的字节。需要用unsafe{}关键字标记,通常用于处理非托管。
winform项目中遇到过,很不建议c#程序员操作内存,非常难定位崩溃原因。

1
2
3
4
5
6
public unsafe void swap(int* p, int *q)
{
int temp = *p;
*p = *q;
*q = temp;
}

NGen.exe工具

.Net Framework提供的NGen.exe工具,可以在应用程序安装到用户计算机时,将IL代码编译成本机代码。
直接使用本机代码可以跳过编译步骤。但在本机代码文件缺失的地方,仍然会去用JIT编译IL代码。
是否用它的利弊很难平衡。启动速度是快了,但如果本机环境一变(比如换了cpu、windows版本等),就会仍然需要JIT编译。而且本机代码,无法像JIT一样对本机做出许多假定,就会少很多优化。

Framework类库

可以了解下Framework都能做什么。

  • Web服务 这个用的是ASP.Net,对应的是.NET CORE框架,和framework区别还挺大的。
  • GUI 应用程序 一般说的就是wpf、winform,官方提供的,主要写PC客户端程序。
  • 控制台程序
  • Windows服务 通过SCM(windows服务控制管理器)控制,这个不太懂
  • 数据库存储过程
  • 组件库 就是自己写的单独程序集,导出,以后当插件用了

CTS规范

通用类型系统 Common Type System,没啥难点,应该是很早的一个规范了。说的就是,

  • 类成员需要按照Field、Methond、Property、Event这几个去写。
  • 访问规则要定,就是public、protected那几个。

每个不同的语言都拥有自己对类不同的写法和特点,但即使代码不同,它们的行为(也就是想让它实现什么)是可以完全一致的。

大章2:生成、打包、部署、管理应用程序

编译过程

C#编译器在执行一个像下面一样的代码时,

1
2
3
4
5
6
// Program.cs :
public sealed class Program{
public static void Main(){
System.Console.WriteLine("Hello World");
}
}

因为,调用了System这个包,所以为了生成这个示例应用程序需要在命令行输入:
csc.exe /out:Program.exe /t:exe /r:MSCorLib.dll Program.cs
这个命令行指示C#编译器生成名为Program.exe的可执行文件(/out:Program.exe),类型是32位控制台应用程序类型exe。
其中MSCorLib.dll因为太常用了,会自动引用,所以不写也行。

响应文件

响应文件就是个cmd命令集的可执行程序,执行它就相当于对控制台逐行输入命令。
一键设置参数省力用的,它长下面这样:

1
2
3
// CSC.exe :
/out:MyProject.exe
/target:winexe

当用命令行执行响应文件CSC.exe时,还会去其所在目录寻找文件CSC.rsp,里面放你想要全局都引用的dll。

元数据

在说元数据之前,先聊一下Program.exe文件,托管PE文件(可移植执行体)由4部分构成:

  • PE32(+)头: 本篇开头提过,不多说。

  • IL: 前面也讲过了。

  • CLR头: 一个小的信息块,包含需要CLR的功能的模块。还是个模块的集合呗。可以查看CorHdr.h头文件定义的IMAGE_COR20_HEADER来了解CLR头的具体格式。

  • 元数据: 是由几个表构成的二进制数据块。分三种,定义表、引用表和清单表。图放在下面,看看有哪些类型的表来理解下意图就完了。


还有例案元数据代码在书35p,这个很推荐自己看代码,下面只记一个重点。
以上面写的那个 Program.exe (就那个Hello World)为例,经过ILDasm.exe整理+美化过的元数据表如下:

注意划线的两块,一个是公共静态方法Main,一个是编译器自动生成的默认类构造器。
Main,公共静态方法,用IL代码实现,返回类型void,无参。
构造器,公共方法,也用IL代码实现,返回类型void,无参。有一个this指针,指向调用方法时构造对象的内存。

程序集

上面说的Program.exe不只是PE文件,也是个程序集(assembly)。上面上的元数据表的第三个清单表没讲,在这里引入:清单表包含一下3个信息,

  • 定义了程序集的可重用的类型
  • 用一个版本号标记程序集
  • 可以关联程序集的安全信息

说的有点复杂了,从用途来看,把它当一个EXE或者DLL就行了。就和DLL一样,如果缺失就会去URL指向的位置下载文件到缓存中,如果仍然没有,就会抛出FileNotFoundException异常。

版本号

微软采用的就是如下的版本编号方案,建议个人也统一标准采用这个。

比如2.5.719.2,2.5是公众对版本的认知;而719是程序集的build号,如果公司每天都生成程序集,那它应该每天递增;revesion是指出当前build的修订次数,如果公司某天内必须生成2次程序集,那revision该递增。

剩下2.7 - 2.8(讲config配置文件)实在是太枯燥,看完一遍也整理不出什么,就这样吧。

大章3:共享程序集和强命名程序集

两种程序集,两种部署

to be continued… // 粗略看了一遍,感觉最后再看比较好