Done is better than perfect

0%

聊聊.Net开发平台

介绍

.Net是什么呢?.Net是1998年微软剑桥研究院的技术人员研究的下一代开发技术,并将其制定的规范(CLI)提交到了ECMA,形成了ECMA335规范, 随后被ISO采纳为国际标准ISO/IEC 23271:2012。到2002微软正式发布了.NET Framework 1.0。.NET Framework是.Net开发技术规范的第一个实现,所以在初期.Net和.Net Framework指的是同一个东西,但它们本质上是完全不同的。由于当时只有微软的.Net Framework实现了.Net标准所以也仅限于Windows开发栈。.Net Framework平台的内容包含很多组件和库,像WinForm, WebForm, WPF,Asp.net等。直到2004年Mono让其可以在Linux上运行了,慢慢的Mono也开始支持Android, iOS,MacOSx让.Net实现跨平台。微软为了让.Net能够在多个平台上运行,在2014公布了.Net Core计划,并于2016年发布了第一个.Net开源版本.Net Core1.0该版本主要实现了Asp.net, 后陆续加入了WinForm和WPF的支持(仅在Windows平台下),意在将.Net Framework的功能移植成开源跨平台的版本,当然.Net Core 也是.Net的未来。为了快速的整合.Net的生态,微软在2016年收购了Xamarin(Mono), 并将Xamarin的开发工具集整合到了Vistual Studio中,至此微软统一了.Net生态。废话说完,下面进入正题。聊一聊.Net中的相关概念和术语,相信大家也经见过一些,比如:CLI、CIL、CTS、CLS、CLR、JIT、BCL、FCL、Module、Assembly 等,本文不会安字典顺序来一一讲解,因为这样大家很难理解也很难记住,本文将通过大家熟悉的HelloWorld程序(基于.Net Core 5.0)来进行解释,欢迎主角登场!

HelloWorld粉墨登场

本文实例基于 .Net core 5.0运行,我们先来创建一个控制台工程命名为HelloWorld,命令如下:

1
dotnet new console -o ./HelloWorld
创建完成后将创建如下目录结构,如下图:

dotnet初始项目结构
  • HelloWorld.csproj 项目文件
  • Program.cs 默认入口代码Main函数
  • obj 临时文件

至此HelloWorld程序就完成了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Program.cs
using System;

namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

这个代码虽然简单,就在控制台打印了一个“Hello World!”. 那这个Hello World有什么怎么被打印出来的呢?里面的Console.WriteLine有哪里来的呢?这个代码编译后会什么内容呢?会和C/C++编译出来的东西一样吗?这个代码又是如何运行起来的呢?这里面就要涉及到.Net规范的核心内容了,下面我们就通过这个Hello World程序来进行介绍。

在HelloWorld目录里,运行如下命令将HelloWorld程序编译出来,命令如下:

1
dotnet build -c "Release"
编译后将生产一个HelloWorld.dll的文件, 此文件就是一个程序集(Assembly)。那么这个程序集的结构是什么样的呢?

Assembly

Assembly包含了哪些内容呢?Assembly是一个自诉型程序集,主要有下面2类

  1. Manifest 清单部分描述了Assembly自身的一些基础信息,包括版本信息,以及对模块和其他程序集的引用关系等;Assembly至少包含一个Module,Module又是代码(CIL)的集合。
  2. 元数据 元数据描述了程序集拥有哪些类型,类型的成员,以及成员的可见性等。
  3. CIL代码,详细信息参见下一节

我们可以通过安装ILDASM工具来查看Assembly里的信息,命令如下:

安装

1
dotnet tool install -g dotnet-ildasm

查看程序集

1
dotnet ildasm HelloWorld.dll -o ./HelloWorld.il

我们先目睹一下HelloWorld Assembly里的Manifest信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.assembly extern System.Runtime
{
.publickeytoken = ( B0 3F 5F 7F 11 D5 0A 3A ) // .._.....
.ver 5:0:0:0
}

.assembly extern System.Console
{
.publickeytoken = ( B0 3F 5F 7F 11 D5 0A 3A ) // .._.....
.ver 5:0:0:0
}

.assembly 'HelloWorld'
{
.custom instance void class [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) // ........
.custom instance void class [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78 63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 ) // ....T..WrapNonExceptionThrows.
.custom instance void class [System.Runtime]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = ( 01 00 18 2E 4E 45 54 43 6F 72 65 41 70 70 2C 56 65 72 73 69 6F 6E 3D 76 35 2E 30 01 00 54 0E 14 46 72 61 6D 65 77 6F 72 6B 44 69 73 70 6C 61 79 4E 61 6D 65 00 ) // ....NETCoreApp.Version.v5.0..T..FrameworkDisplayName.
.custom instance void class [System.Runtime]System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 0A 48 65 6C 6C 6F 57 6F 72 6C 64 00 00 ) // ...HelloWorld..
.custom instance void class [System.Runtime]System.Reflection.AssemblyConfigurationAttribute::.ctor(string) = ( 01 00 07 72 65 6C 65 61 73 65 00 00 ) // ...release..
.custom instance void class [System.Runtime]System.Reflection.AssemblyFileVersionAttribute::.ctor(string) = ( 01 00 07 31 2E 30 2E 30 2E 30 00 00 ) // ...1.0.0.0..
.custom instance void class [System.Runtime]System.Reflection.AssemblyInformationalVersionAttribute::.ctor(string) = ( 01 00 05 31 2E 30 2E 30 00 00 ) // ...1.0.0..
.custom instance void class [System.Runtime]System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 0A 48 65 6C 6C 6F 57 6F 72 6C 64 00 00 ) // ...HelloWorld..
.custom instance void class [System.Runtime]System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 0A 48 65 6C 6C 6F 57 6F 72 6C 64 00 00 ) // ...HelloWorld..
.hash algorithm 0x00008004
.ver 1:0:0:0
}

.module 'HelloWorld.dll'
// MVID: {70604f0b-74ab-4028-87d6-2026571d0897}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WindowsCui
.corflags 0x00000001 // ILOnly

CIL——公共中间语言

C#编译器将C#代码编译成包含了CIL的程序集(Assembly),其他的编程语言比如VB,F#等,通过自己的编译器将源码编译成CIL,那些能够被编译成CIL的编程语言我们统称为面向.Net的语言,正因为有CIL中间语言的存在才使得.Net能够实现跨语言编程。

接下来我们看哈上面的C#的HelloWorld程序的CIL代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.class private auto ansi beforefieldinit HelloWorld.Program extends [System.Runtime]System.Object
{
.method private hidebysig static default void Main(string[] args) cil managed
{
// Method begins at Relative Virtual Address (RVA) 0x2050
.entrypoint
// Code size 11 (0xB)
.maxstack 8
IL_0000: ldstr "Hello World!"
IL_0005: call void class [System.Console]System.Console::WriteLine(string)
IL_000a: ret
} // End of method System.Void HelloWorld.Program::Main(System.String[])
.method public hidebysig specialname rtspecialname instance default void .ctor() cil managed
{
// Method begins at Relative Virtual Address (RVA) 0x205C
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void class [System.Runtime]System.Object::.ctor()
IL_0006: ret
} // End of method System.Void HelloWorld.Program::.ctor()
} // End of class HelloWorld.Program

下面我们来分析一下“公共中间语言”的“公共”,“中间”和“语言”都代表的什么

公共

公共指的是通用的意思,也就是说不论你用哪种语言编写的代码最终编译出的都是通过CIL来描述的,我们新建一个VB的HelloWorld工程,命令如下:

1
dotnet new console -lang VB -o ./HelloWorldVB
VB代码如下:
1
2
3
4
5
6
7
Imports System

Module Program
Sub Main(args As String())
Console.WriteLine("Hello World!")
End Sub
End Module

编译后将HelloWorldVB.dll通过ildasm工具反编译成CIL,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.class private auto ansi sealed HelloWorldVB.Program extends [System.Runtime]System.Object
{
.custom instance void class [Microsoft.VisualBasic.Core]Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute::.ctor() = ( 01 00 00 00 ) // ....
.method public static default void Main(string[] args) cil managed
{
.custom instance void class [System.Runtime]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) // ....
// Method begins at Relative Virtual Address (RVA) 0x2050
.entrypoint
// Code size 11 (0xB)
.maxstack 8
IL_0000: ldstr "Hello World!"
IL_0005: call void class [System.Console]System.Console::WriteLine(string)
IL_000a: ret
} // End of method System.Void HelloWorldVB.Program::Main(System.String[])
} // End of class HelloWorldVB.Program
可以看出C#和VB编译出来的CIL代码基本上一样只是C#多生成了一个类的构造函数。因为VB是面向过程的编程所以不存在类和类的构造函数这一说。现在我们应该明白公共代表什么意思了吧!

中间

为什么是“中间”呢?CIL不像C/C++静态语言会直接编译链接成CPU能够直接执行的机器码,而是需要一个公共语言运行时(CLR)来进行及时的编译(JIT)。所以将其称为中间语言。JIT编译器的根据执行编译阶段大致可以分为3类:

  1. Pre-JIT 编译器,在程序集部署时进行编译,就是通过一个Ngen.exe(本地代码生成器)将CIL转换为本地机器码。
  2. Normal JIT 编译器,在程序集首次调用的时候将其编译成本地机器码并缓存。
  3. Econo JIT 编译器,在方法执行前进行编译,执行完后删除,dotnet2.0后就使用的此类型的JIT

语言

CIL其实本身也是一门基于堆栈的编程语言,只是相比C#要稍微低级(并不是很low的意思)一些,我们也可以直接写CIL代码然后使用ILASM工具将其转换成程序集在CLR中运行。

我们在回头看看HelloWorld程序,里面调用了一个Console.WriteLine函数,那这个函数又是哪里来的呢?

BCL和FCL

BCL-基础类库

我们在开发一个程序时,不可能任何基础功能都功能都从零开始,所以各个.Net的实现平台都为我们定义一个基础功能类库BCL, 比如像组数,链表,字典, Console,包括基本原类型byte,short, long这些都是基于System下的System.Byte,System.Int16和System.Int64,当然还包含了些系统相关的功能比如线程,安全性等。但是各个平台的对这些接口的实现也有所差异,所以为了实现各个平台的通用性,微软在自家产品中率先制定了一个可移植类库PCL,取各个平台的公用部分形成,结构如下图: pcl 随着实现.Net平台的增多,为了更好跨平台性微软制定了基础库的标准.Net Standard,所有.Net实现平台上的BCL接口都必须遵守.Net Standard中制定的标准,这样便可以让在各个平台开发时调用的基础库的接口都是统一的。虽然BCL构成了我们编程的基础库,但是比如我们要开发一个Windows的应用,那么界面该如何搭建呢,界面上又有哪些控件呢?接下来就要介绍框架类库-FCL了。FCL和.Net Standard关系图如下: fcl

FCL-框架类库

BCL只是框架类库的一个子集而已,FCL包含的功能巨多,也是我们经常使用的类库,每个FCL的子库都够写一本书的了,我们根据功能可以将FCL大致分为一下3层。

  • 最内一层,由BCL的大部分组成,主要作用是对.NET框架,.NET运行时及CIL语言本身进行支持,例如基元类型、集合类型、线程处理、应用程序域、运行时、安全性、互操作等。
  • 中间一层,包含了对操作系统功能的封装,例如文件系统、网络连接、图形图像、XML操作等。
  • 最外一层,包含各种类型的应用程序,例如Windows Forms、Asp.NET、WPF等。

CTS-公共类型系统

如果我们要开发一门像C#或VB一样的语言,在编译后也生成CIL语言,在.Net环境中运行,那么我们这个语言具有哪些特性就不是我们语言所能决定的了,而是有CIL所定义的规则决定的,而这些定义的规则就是CTS,CTS中定义了类,接口,结构体也定义了类里能包含属性,字段,函数,事件等,也定义了类只能继承一个父类,可以实现多个接口。C#和VB就是微软定义的符合CTS规则的语言。

CLS-公共语言规范

假设我们有3门面向.Net的语言,如果3门语言公开(Public)部分需要相互调用,那么他们必定需要遵循一定规则,这个规则就是CLS,CLS是CTS的一个子集,各个面向.Net的语言需要准守这个规范,否则就不就会存在互调的兼容性问题。CLS具体有哪些规则呢?是否区分大小写,标识符的命名规则如何,可以使用的基本类型有哪些,构造函数的调用方式(是否会调用基类构造函数),支持的访问修饰符等。

CLR-公共语言运行时

前面我们了解了.Net SDK编译出来的程序集(Assembly)的相关信息,下面我们将接着讨论,编程出来的程序集是怎么运行起来的呢?这就的归功于CLR,CLR其实就是一个能够执行CIL的虚拟机,其主要功能包括:管理应用程序域、加载和运行程序集、安全检查、JIT(将CIL代码即时编译为机器代码)、异常处理、对象析构和垃圾回收等。现在我们了解了CLR的功能,但是还是不知道它是怎么运行起来的。要说清楚这个问题我们必须先看看在系统中可执行文件的格式,在MacOs中是Mach-O格式,在Windows中是PE/COFF格式,在Linux是ELF格式,我们详细看下Mach-O文件吧,其他的格式都大同小异。Mach-O格式如下: Mach-O

Mach-O 的组成结构如图所示包括了:

  • Header 包含该二进制文件的一般信息 字节顺序、架构类型、加载指令的数量等。 使得可以快速确认一些信息,比如当前文件用于 32 位还是 64 位,对应的处理器是什么、文件类型是什么

  • Load commands 一张包含很多内容的表 内容包括区域的位置、符号表、动态符号表等。

  • Data 通常是对象文件中最大的部分 包含 Segement 的具体数据

每个段的具体信息可以通过otool工具进行查看。 系统运行Mach-O文件大致步骤:

  1. 系统把Mach-O文件加载进入内存,
  2. 检查头看是否是相同CPU架构是否和符合当前的系统需求
  3. 链接动态库
  4. 找到入口程序(Main函数)的地址开始执行

我们发布一下HelloWorld程序,代码如下:

1
dotnet publish -r osx-x64 -c Release --self-contained
我们通过自包含的方式发布,这样就不需要目标机器上安装.Net运行时了。我们来看一下发布后的文件有哪些,如下图: 发布类容

重点是HelloWorld文件,这个文件是启动文件,这是一个Mach-O格式的文件,就想普通的MacOs系统的可以执行文件一样,他会链接像libclrjit.dylib,libcoreclr.dylib的CLR相关动态库文件,这样当HelloWorld通过系统调起的时候便会启动CLR, 并执行HelloWorld.dll中的主函数。我也可以直接使用dotnet HelloWorld.dll执行我们的程序集,因为dotnet这个Mach-o格式文件会去链接CLR相关的库。

总结

此篇文章主要讲了CLI规范中的相关术语,并通过一个HelloWorld的程序了解了一个CIL程序集时如何被编译生成的,也了解了一个程序集的内部结构,对.Net平台也有更深入的理解。