读书人

[原创]探索CLR原理系列(一):类型 (适

发布时间: 2013-03-06 16:20:31 作者: rapoo

[原创]探索CLR原理系列(1):类型 (适合老鸟,新人勿沉迷其中)

CLR是整个Dotnet的灵魂,CIL则是这个灵魂可以发挥其跨越平台,穿越语言,跳跃....的保证.其实有很多书籍和文章都介绍了什么是CLR,什么是CIL,CTS,CLS这样的一大堆概念,可是他们具体的表现形式,以及运作的原理是大部分人都想知道的秘密,却没有什么太好的途径来获取这些信息.本系列将从C#代码->CIL->CLR来探索我们编写的C#代码,最终如何成为本地机器语言,并且执行.过程中会使用VS2010+SOS调试.关于SOS调试的详细内容,大家可以上网搜索,这里不再赘述.一直想写这个系列的文章,可是内容太多,其中各部分之间的相关性很复杂,让我感觉无从下手.终于决定先硬着头皮写一篇看看,否则可能永远也不知道该从哪里些起.文章中的内容都是笔者大量的阅读书籍以及从网络搜索资料加上自己的试验,调试所理解到的东西,笔者能力有限,如有误导观众之处,欢迎大家指出.我们可以一起讨论,一起来完善这些知识. 先给大家一个图,笔者吐血画出来的,可以先看看Load堆中类型大致是一个怎么样的状态。图中的其他内容,笔者将陆续介绍给大家,这个图只是其中的一部分,还有线程栈的部分,由于图太大,等说到那部分再发吧.图中的MatedataToken就是我们今天介绍的从IL到CLR,类型的标记的一部分,即TestObjectType1的类型标记码(2000003)。

[原创]探索CLR原理系列(一):类型   (适合老鸟,新人勿沉迷其中)

由于某些媒体和个人喜欢拿来主义,所以笔者加了水印,见谅见谅。。。不影响大家看就是了,字比较小,可将浏览器放大。

今天先来说说.net中的类型,我们先不去区分什么值类型在声明它的地方,引用类型类型的实例在GC堆中.实际在CLR中有3个堆(GC堆,Load堆,大对象堆),我们今天要描述的是Load堆用来存放类型而不是类型的实例,关于GC堆是后面要做的事情.只说类型,首先看一下笔者定义的类型.


我们现在来看看CLR如何加载一个类型.

1 TestObjectType1 obj = new TestObjectType1();
2
3 TestValueType1 val = new TestValueType1();4

相关IL


IL_0001: newobj instance void TestDemo1.Type.TestObjectType1/*02000003*/::.ctor()

IL_0010: initobj TestDemo1.Type.TestValueType1/*02000004*/

原来在IL代码中是使用类型标记码来标记语句,然后CLR通过标记加载它的.我们使用SOS来调试一下,看看在CLR在内存中把类型表现为什么样子.


TestObjectType1 obj = new TestObjectType1(); //调试C#代码的当前语句
TestValueType1 val = new TestValueType1();

0022eb9c 0037009e TestDemo1.Type.TestMain.Main()
LOCALS://线程栈地址 0x0022eba8 = 0x00000000//GC堆地址(引用类型) or 线程栈地址(值类型)

//可以看到引用类型也被初始化为0,但它是一个地址,地址为0也就是引用不存在,为null

0x0022eba4 = 0x00000000 //这里可以看到值类型被默认初始化为0

可以看到当new还没有执行时, 0x0022eba8(obj )指向的值是0,由于地址为0的引用就是不存在的,所以等于null.val 的值等于0,也就是说在线程栈上,没有东西可以为null,所有的东西都有值,这也就是为什么值类型不需要构造函数也一样可以初始化.没有人可以阻止值类型的初始化.

TestObjectType1 obj = new TestObjectType1();
TestValueType1 val = new TestValueType1(); //调试C#代码的当前语句
LOCALS:
0x0022eba8 = 0x0240b928
0x0022eba4 = 0x00000000

在new执行后,obj的内存在GC中被分配,可以看出new关键字分配了内存,并将分配好的内存地址返回给栈上的地址空间。现在我们来看看0x0240b928这块内存空间有什么?

!dumpobj 0x0240b928

Name: TestDemo1.Type.TestObjectType1
MethodTable: 001538f4 //方法表
EEClass: 00151564 //类型关系图
Fields:none //字段

可以看到在在这块地址上有方法表的地址,字段列表的地址,以及类型继承关系的地址。让我们一个一个来看,首先EEclass

!dumpclass 00151564
Class Name: TestDemo1.Type.TestObjectType1
mdToken: ed13f79202000003 //红色部分是Dll被加载到虚拟内存空间的地址
Parent Class: 6c1a3ef8 //父类的地址
Method Table: 001538f4 //方法表的地址,与上面的指向是一样的
Vtable Slots:4//4个虚方法

Total MethodSlots:5//方法表中的方法个数

ClassAttributes:100001//类型的类别 (引用类型)

NumInstanceFields:0//实例字段的个数

NumStaticFields: 0 //静态字段的个数

MdToken是记录了类型在元数据表里(IL编译后的类型标记码)的标记。这样我们在使用反射时,就可以很快地定位到元数据的信息,前面的那一部分(ed13f792)是dll在虚拟内存中的地址.

例如 System.Type t = typeof(TestObjectType1); 将会定位到类型的mdToken,然后找到类型所在Dll中的元数据表Typedef,根据2000003这个主键找到元数据,读取并返回一个Type类型的实例 t,到当前线程栈的内存空间中。

接下来我们看看父类的内容,ParentClass

!dumpclass 6c1a3ef8

ClassName: System.Object

//红色部分是Dll被加载到虚拟内存空间的地址
mdToken: f5dcf17a02000002 //注意TypeRef的Token,并不是Type的真实Token,这里是Object在MSCorlib.dll中的真实标记。

Parent Class: 00000000//没有父类

MethodTable: 6c4bf5e8 //方法表

Vtable Slots:4//4个虚方法

Total MethodSlots: a //总计10个方法

ClassAttributes:102001

NumInstanceFields:0//无任何字段

NumStaticFields:0

注意TypeRef的Token(01000001),并不是Type的真实Token,这里是Object在MSCorlib.dll中的真实标记。Object是顶级类,所以他没有父类,也没有任何字段,但是它有虚方法。注意看一下,TestObjectType1没有任何成员,他完全继承了Object,但是他们的方法表地址却不同。关于方法表将在后续的文章中介绍。那么值类型会是什么样呢?我们继续调试

TestValueType1 val = new TestValueType1();

Console.Read();

LOCALS://当前线程栈地址

0x002cf1c4 = 0x00000000 //由于结构没有定义任何字段所以默认为0

在这里我们没有定义任何字段,上面的元数据中我们看到了值类型不定义任何字段,它的大小也为1,因为值类型不可能为Null,所以它不可能没有大小,也就是说值类型在栈上只要分配了,就一定要有值.

关于值类型和引用类型在栈上的状态,我们以后会继续分析,这里为了简化仅仅是点到为止.

这里为了可以看到值类型实例的类型在内存中的状态,笔者只有将它装箱后才能根据它栈中指向GC堆的地址来拿到它类型的地址,具体原因我们在以后介绍.

TestValueType1 val = new TestValueType1();
object obj = val; //将值类型装箱

Console.Read(); //调试当前点

!dumpobj 0x023cb928Name: TestDemo1.Type.TestValueType1MethodTable: 00313894EEClass: 003114e0

Fields: None

这里可以看到装箱后我们找到了实例(这时候是一个引用了)的地址,并且跟踪到了类型.我们来看看它的EEClass是怎样的?

!dumpclass 003114e0
Class Name: TestDemo1.Type.TestValueType1
mdToken: d6f025102000004 //与元数据中的类型标识码一样
Parent Class: 6b908a10
Module: 00312e9c
Method Table: 00313894
Vtable Slots: 4
Total Method Slots: 4
Class Attributes: 100109 //代表是值类型的布局方式
NumInstanceFields: 0
NumStaticFields: 0


与引用类型相比只有类型布局不一样,而且方法表的总数不一样,这是因为值类型编译器不会生成默认构造函数,而我们上面可以用new完全是c#给我们的一种语法糖而已。

我们来看看它的基类吧。

!dumpclass 6b908a10
Class Name: System.ValueType
mdToken: 9590d7902000009
Parent Class: 6c1a3ef8 //都是Object
Method Table: 6bbcf730
Vtable Slots: 4
Total Method Slots: 5
Class Attributes: 102081 Abstract, 不可被实例化
NumInstanceFields: 0
NumStaticFields: 0

它的父类是ValueType,那么Valuetype的父类呢, 和上面TestObjectType1的ParentClass地址一样。说明了什么呢? 仔细看这里你会发现,他总共有5个方法,其中4个是虚的,那是从Object继承下来的,但是它的子类TestValueType1却只有4个方法。为什么?因为ValueType有一个受保护的构造函数,而构造函数是不继承的,那么为什么是受保护的?因为当你定义抽象类后,会在编译时默认的生成受保护的构造函数,自ValueType以后,它的所有子类都表现为了值类型的特性,没有默认构造函数,在声明的地方初始化,即可能在栈中,也可能在GC堆中。

总结:

当代码编译为dll时,每一个类型在元数据的Typedef表中,会分配一个MdToken(类型标记),当你写的方法需要访问这个类型时,也是使用MdToken到相关Dll的元数据表去加载它到Load Heap,LoadHeap是用来存放类型的空间,它并不保存类型的实例.当clr加载完类型后,就会根据你的代码初始化值类型或者引用类型,clr会根据元数据表中父类类型object,valuetype来区分是引用类型,值类型到相应的地址空间(GC堆或线程栈) ,实例化这部分内容,我们在后续的文章中继续讨论.在C#的类型中,我们可以定义字段,属性,方法,事件和嵌套类,但我们跟踪类型的EEclass,发现类型中只有两类成员,字段(事件就是一个委托,而委托只是一个类型,所以事件就是一个字段而已,但表现有些特殊后续介绍)和方法(属性实际就是方法).

在后续的文章中我们将陆续的研究字段,方法,然后再回过头来谈论类型和类型的实例(Gc堆和栈),反射以及垃圾回收器,异常管理等内容.

读书人网 >编程

热点推荐