揭开CLR神秘面纱—第一步PE文件
熟悉.NET架构的都对CLR,CTS,IL等概念会很熟悉,但是对这些概念真正了解的很深入的人估计没有很多。在学习之余,想把自己对CLR的学习心得拿来跟大家沟通一下。
CLR(Common Language Runtime)提供了.NET应用程序都要使用的编程模型,包括文件加载器,内存管理器,安全系统,线程池等。并且能支持多种语言运行的一个平台。
我们都知道,每一种语言都有其自身的编译器,如C#有C#编译器。C有C编译器,那这些不同编译器编译出来的汇编指令或机器指令如何能统一在这么一个CLR环境中运行呢?
大家都会回答,这是IL的功劳。所有语言经过编译后都会转化成IL。没错,但是这并不全面。这些不同的语言之所以能在CLR中运行。单靠IL肯定是不行的。
事实上在.NET中,不管使用哪一种编译器,在CLR中其结果都是一个manage module(托管模块),这个模块就是我们标题里说的PE文件。PE(Portable
Excutable)可移植执行体。
那么PE里面又包含哪些东西呢?
一个托管PE文件通常由4个部分构成:PE头,CLR头,元数据以及IL(中间语言)
那我们如何获取这些信息呢?我们开始一段最简单的代码。
1 class Program
2
3 {
4
5 static void Main(string[] args)
6
7 {
8
9 Console.WriteLine("Hello world");
10
11 }
12
13 }
14
然后我们利用ildasm打开他的中间语言。选择视图—》统计。会看到如下代码。
其中PE headersize就是PE头信息,CLR header size就是CLR头信息,meta-data size为元数据信息。Manage
code就是IL信息。这几个部分分别起什么作用呢?
PE头:标准的windows PE文件头,分为PE32和PE32+两种格式,如果这个头使用PE32格式,则咱们的托管模块可在windows32位和64位版本上运行,如果标识为PE32+格式,则托管模块就只能在windows64位系统上运行。它还包含了文件类型,如是DLL还是GUI。如果可能,还会包含与本地CPU代码有关的信息。
CLR头:包含所要求的CLR版本,一些标志,托管模块入口方法(Main方法)的元数据标记。以及模块的元数据,资源等等信息。
元数据:其实对元数据这个词本身不好理解,我所理解的元数据就是表。这些表里模块本身的定义的一些类型和成员,就是定义表。还有包含引用的一些对象称之为引用表和清单表。关于这些可能会在以后专门介绍一下。
至于IL可能就不要多说了。就是咱们前面说的不同的编译器编译后生成的代码,程序运行的时候,CLR会将IL编译成本地CPU指令。
综合所述:代码被编译后,生成的PE文件中会包含以上所说的四个部分,这四个部分中PE头,CLR头和元数据记录了我们的程序运行的环境、程序运行时所需要的对象资源,这些资源通常分配在我们计算上的内存上。最后CPU指令把这一系列复杂的东西运行起来。就是我们的程序运行起来了。当然,这只是一个笼统的说法,其中很多的细节是如何处理的呢?线程如何执行?垃圾又是如何实现自动回收的呢?在以后的章节中,我们会继续学习这些。
揭开CLR神秘面纱-----第二步 垃圾回收之(制造垃圾)
阅读本章前,要求读者对值类型与引用类型有相当的了解
关于值类型与引用类型的深入讲解,请参见ANYTAO的[你必须知道的.NET] 第八回:品味类型---值类型与引用类型(上)-内存有理
----.NET应用程序初始化的时候所有的引用类型都会被分配到托管堆上面。而只有托管堆里面的资源才能被垃圾回收机制给管理起来。所以,我们要明白,CLR的垃圾回收机制并不是针对所有数据的。我们该手动释放的时候必须得要手动释放。
我们从何得知这一结论?如何论证这一点?
程序是如何在CLR中运行的呢?那我们CLR中垃圾回收机制又是怎么样运行的呢?我们是否需要再进一步去研究呢?我们如果不手动释放值类型,它们会常驻我们宝贵的内存空间吗?我不得不承认,这里牵扯到了很复杂的算法和操作系统及Win32编程的相关知识。这可能需要大篇幅的文字来说明这些问题。
既然有垃圾回收,肯定就有垃圾制造者在制造垃圾。这些垃圾是如何被制造出来的,就是我们今天试图弄清楚的问题。希望能通过以下文字描述清楚,程序运行时各阶段的内存使用状态。
请看下面的代码:
class Program
{
static void Main(string[] args)
{
//值类型
char i = new char();
char j = 'a';
//引用类型
StringBuilder sb = new StringBuilder();
//自定义类型
MyClass mc = new MyClass();
//系统的ENUM类型
StringComparison sc;
//自定义的enum类型
MyEnum me = MyEnum.test;
//结构体
MyStruct ms1;
ms1.T = "hi";
//在这里进行的对象复制
MyStruct ms2 = ms1;
ms1.T = "hi2";
MyStruct ms3 = new MyStruct();
char k = 'b';
if (ms2.Equals(ms1))
{
Console.WriteLine("实例相同" + ms2.T);
}
Console.WriteLine(ms1.T);
Console.WriteLine(ms2.T);
Console.WriteLine(ms3.T);
}
}
class MyClass
{
}
enum MyEnum
{
test,
}
struct MyStruct
{
public string T;
}
很明显,上面定义了一个类,一个枚举,一个结构,这是一段表面看似很普通的代码,但其实骨子里却大有乾坤。我们再次打开ILdasm来看下它的真面目。
可以看的出,两个类都有.ctor,就是说,两个类都有构造函数。点开会发现原来是继承自Object的构造函数。这也就解释了为什么我们定义类的时候就算不显示声明构造函数,系统也会自动给你声明构造函数。功劳来自于CLR.
细心的朋友又会发现另外一个问题,那这里这个结构明明没有看见构造函数,那它为什么也能new呢?关于这里,可以参考anytao关于new的文章
我这里谈谈自己的看法。我们点开Main函数。会看到如下内容
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 2
.locals init ([0] char i,
[1] char j,
[2] class [mscorlib]System.Text.StringBuilder sb,
[3] class CLR.MyClass mc,
[4] valuetype [mscorlib]System.StringComparison sc,
[5] valuetype CLR.MyEnum me,
[6] valuetype CLR.MyStruct ms1,
[7] valuetype CLR.MyStruct ms2,
[8] valuetype CLR.MyStruct ms3,
[9] char k,
[10] bool CS$4$0000)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: ldc.i4.s 97
IL_0005: stloc.1
IL_0006: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
IL_000b: stloc.2
IL_000c: newobj instance void CLR.MyClass::.ctor()
IL_0011: stloc.3
IL_0012: ldc.i4.0
IL_0013: stloc.s me
IL_0015: ldloca.s ms1
IL_0017: ldstr "hi"
IL_001c: stfld string CLR.MyStruct::T
IL_0021: ldloc.s ms1
IL_0023: stloc.s ms2
IL_0025: ldloca.s ms1
IL_0027: ldstr "hi2"
IL_002c: stfld string CLR.MyStruct::T
IL_0031: ldloca.s ms3
IL_0033: initobj CLR.MyStruct
IL_0039: ldc.i4.s 98
IL_003b: stloc.s k
IL_003d: ldloca.s ms2
IL_003f: ldloc.s ms1
IL_0041: box CLR.MyStruct
IL_0046: constrained. CLR.MyStruct
IL_004c: callvirt instance bool [mscorlib]System.Object::Equals(object)
IL_0051: ldc.i4.0
IL_0052: ceq
IL_0054: stloc.s CS$4$0000
IL_0056: ldloc.s CS$4$0000
IL_0058: brtrue.s IL_0073
IL_005a: nop
IL_005b: ldstr bytearray (9E 5B 8B 4F F8 76 0C 54 ) // .[.O.v.T
IL_0060: ldloca.s ms2
IL_0062: ldfld string CLR.MyStruct::T
IL_0067: call string [mscorlib]System.String::Concat(string,
string)
IL_006c: call void [mscorlib]System.Console::WriteLine(string)
IL_0071: nop
IL_0072: nop
IL_0073: ldloca.s ms1
IL_0075: ldfld string CLR.MyStruct::T
IL_007a: call void [mscorlib]System.Console::WriteLine(string)
IL_007f: nop
IL_0080: ldloca.s ms2
IL_0082: ldfld string CLR.MyStruct::T
IL_0087: call void [mscorlib]System.Console::WriteLine(string)
IL_008c: nop
IL_008d: ldloca.s ms3
IL_008f: ldfld string CLR.MyStruct::T
IL_0094: call void [mscorlib]System.Console::WriteLine(string)
IL_0099: nop
IL_009a: ret
} // end of method Program::Main
可以看到,在init的时候,里面传入了11个参数。分别就是我们代码里使用的11个对象。Init指令起的作用是“init
specifies that the variables must be initialized to the default
values for their respective types” init 指定的变量必须被各自类型的默认值初始化--原文地址
继续我们上面的问题,结构没有构造函数为什么也能new呢?分析IL代码,发现new class的时候也就是new 引用类型的时候,IL的指令是newobj,并且还instance了相应的构造函数,new
struct的时候,IL的指令却是initobj。这似乎就是问题的根源所在。
先说说newobj指令吧,执行该指令的时候,他会将我们指定的类型实例化,而且根据需要会将新类型中的成员初始化一次,再把这个对象送到托管堆上。然后再根据参数调用构造函数。构造函数对该类型做的操作就是在托管堆中进行了,最后在线程堆上只是把这个对象的引用压栈。(我们的托管堆上其实存在一个指针。该指针就是我们垃圾回收的最高检察官。关于他,咱们以后再做描述。)
那我们的initobj做了什么工作呢?MSDN上给出的解释很简单。该指令就是初始化值类型。因为编译后,非primitive
type的值类型已经存在于线程堆栈上。但是还没有初始化。所以需要做这一步操作。而所有的primitive type在编译时会初始为0.这也就是为什么char和MyStruct同为值类型而char没有init的原因了。
通过上面的描述,我们应该可以清晰的看出.NET CLR中的运行过程,对象的创建是一个递归的过程。恰恰也是咱们通常所说的Builer模式的一个体现。至此,我们的“垃圾”都已经制造完毕,下一步可以讨论生成的垃圾该如何回收了。这一步最重要的理解在于初始化。
MSIL速查手册下载,感谢无私奉献
版权声明:原创技术文章,如需转载,请声明出处。不得用于任何商业形式活动,否则将追究法律责任。 |