CLR 世界简介






4.93/5 (28投票s)
了解 .NET 背后的工作原理非常重要。
引言
.NET Framework,尤其是 C# 语言,因其易用、强大和高效而在世界各地得到广泛应用。但很少有开发者会深入了解 .NET 的工作机制。那么,让我们开始探索这个世界吧。
背景
框架中的各种语言可以互相转换,这在一定程度上得益于一套规则和模式,所有语言都必须遵循这些规则和模式,这些规则和模式在 CTS(通用类型系统)和 CLS(通用语言规范)中进行了规定。框架中的每种语言在编译时都会被转换成一种中间语言,这种语言对框架中的所有高级语言都具有通用的语法。这种语言就是 MSIL(Microsoft 中间语言),当我们用 C# 编写代码时:
public void FooSum()
{
var number1 = 100;
var number2 = 400;
Console.WriteLine(number1 + number2);
}
编译器会将您的代码转换为这种 IL
.method public hidebysig
instance void FooSum () cil managed
{
// Method begins at RVA 0x2070
// Code size 18 (0x12)
.maxstack 2
.locals init (
[0] int32 number1,
[1] int32 number2
)
IL_0000: ldc.i4.s 100
IL_0002: stloc.0
IL_0003: ldc.i4 400
IL_0008: stloc.1
IL_0009: ldloc.0
IL_000a: ldloc.1
IL_000b: add
IL_000c: call void [mscorlib]System.Console::WriteLine(int32)
IL_0011: ret
}
程序经历的基本工作流程
- 代码的开发
- 代码构建时,C# 编译器会将 C# 转换为 IL
- 代码执行时,CLR 会将 IL 代码转换为机器码
需要说明的是,IL 到机器码的转换是按需进行的,也就是说,只有在执行时才会被转换的那部分代码。
注意:Microsoft 即将推出一种名为 .NET Native 的 Windows 应用预编译技术,其主要优势在于能够自动将托管代码编写的应用程序版本直接编译为本机代码,并且无需在客户端计算机上安装 .NET 运行时。
内存管理
为了管理所有这些流程,CLR 对计算机内存进行了抽象,并将其划分为几个部分
- 局部变量:这部分将保存当前执行的方法中创建的所有变量。
- 参数变量:顾名思义,这部分内存用于存放传递给方法的参数。
- 静态属性:所有静态变量。
- 堆栈(Stack):这是最重要的内存区域,因为所有其他区域都将与堆栈“通信”。所有值类型对象都将驻留在此处。实际上,所有对象都会被传递到堆栈,要么是对象本身,要么仅仅是对它的引用。堆栈的大小在入口点(如 main 方法)开始时就被告知。
- 堆(Heap):所有引用类型对象都将驻留在此处,这意味着当我们创建一个新对象时,CLR 所做的是将该对象放入堆中,当该对象即将被使用时,指向该对象的引用(更准确地说,是一个指针)将被放入堆栈。每次需要该对象时,CLR 都会检查堆栈中它的地址,然后跳转到该地址获取信息。
- 动态:这部分将保存编译时大小未知,仅在运行时才确定大小的对象,例如大部分集合。
.NET 中的类型
框架基本上将类型分为两类:值类型和引用类型。
值类型是最“便宜”的对象,因为它直接包含值(即对象本身携带其值),并且分配在堆栈上。值类型不支持派生,每个值类型都派生自 System.Value.Type,并且有一个隐式构造函数来初始化该类型的默认值。最常用的值类型包括:bool
、int
、float
、long
、char
、struct
。当程序需要使用值类型变量时,CLR 会分配其大小并将对象压入堆栈。
引用类型保存在堆内存中,其获取成本更高,因为 CLR 处理引用类型的方式是,每种类型都有 8 字节的开销,用于两个额外的字段:同步块索引(Sync Block Index)和对象类型指针(Object Type Pointer)。其中,对象类型指针最为重要,它指向一个描述该元素对象类型的数据结构。因此,要从堆中获取一个元素,CLR 会检查堆栈中指针指向的地址,然后跳转到该地址获取堆中的值,并检查其类型,甚至会查找对象类型指针中保存的地址。
为了向您证明这一点,我们可以使用 Visual Studio 调试我们的应用程序,一个简单的应用程序,并在调试模式下展示 CLR 的工作原理。让我们开始吧。
Using the Code
让我们创建一个简单的类,名为 Person
。
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
现在,在我们的控制台应用程序的 Program.cs 文件中,创建两个 Person
实例,声明一个 Id
和一个 Name
,以方便学习。
static void Main(string[] args)
{
var person1 = new Person();
person1.Name = "Pedro";
person1.Id = 1;
var person2 = new Person();
person2.Name = "John";
person2.Id = 2;
}
当我们运行应用程序时,让我们在起始行设置断点,并打开一些调试窗口,例如内存窗口和寄存器窗口。当我们实例化第一个 person 对象时,复制 EAX 寄存器的值,并将其粘贴到内存窗口的 Address
文本框中。
现在我们可以在堆内存中看到我们的实例。
在这里,我们可以看到我们的对象 person1
从地址 0x025017D4 开始,到地址 0x025017E0 结束。
- (0x025017D4):初始地址保存了同步块索引,当对象需要某种类型的同步时会使用它。
- (0x025017D8):此地址包含指向表示
Person
类型的数据结构的内存位置。如果我们有两个相同类型的对象,对象类型指针的值将相同,即指向相同的数据结构。 - (0x025017DC):包含该字段在堆内存中的位置,因为它是
string
类型,属于引用类型。 - (0x025017E0):保存了整数本身的值,因为它是值类型。
覆盖所有行后,我们可以看到这些对象是不同的,但类型相同,因为它们指向相同的地址。
就这样,CLR 是一个更广阔的世界,这只是一个关于我们运行代码时发生什么的简介。
参考文献
- Infosec Institute: http://resources.infosecinstitute.com/net-framework-clr-common-language-runtime/
- Elemar Júnior: http://elemarjr.net/
- Alberto Monteiro: http://blog.albertomonteiro.net.br/2013/03/25/conhecendo-intermediate-language-il-revista-net-magazine-99/
- MSDN: https://msdn.microsoft.com/pt-br/library/8bs2ecf4(v=vs.110).aspx
历史
- 2015 年 11 月 5 日:初始版本