随笔 - 26  文章 - 4 评论 - 558 trackbacks - 54


文章转载,请加入本Blog链接
昵称:徐 磊
园龄:3年7个月
粉丝:86
关注:4

随笔分类(28)

文章分类(4)

我的好友

积分与排名

  • 积分 - 103613
  • 排名 - 982

最新评论

   

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

 

 

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

 

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

 

 1     public class TestObjectType1
 2     { }
 3 
 4     public struct TestValueType1
 5     { }
 6 
 7     public interface ITestInterface1
 8     { }
 9 
10     public delegate void TestDelegate1();

  在这里我只定义了类型,类型中没有任何成员,下面我将编译生成程序集,然后来看看CIL中他们是什么样子.

  首先来看看引用类型TestObjectType1

 1 TypeDef #1 (02000002) //类型标记
 2 -------------------------------------------------------
 3     TypDefName: TestDemo1.Type.TestObjectType1  (02000003)
 4     Flags     : [Public] [AutoLayout]//类型字段加载方式 [Class] [AnsiClass] [BeforeFieldInit
 5     Extends   : 01000001 [TypeRef] System.Object //父类
 6     
 7 TypeRef #1 (01000001)
 8 -------------------------------------------------------
 9 Token:             0x01000001
10 ResolutionScope:   0x23000001
11 TypeRefName:       System.Object

 

可以看到在CIL的元数据部分,可以清晰地看到TestObjectType1继承自Object以及它的类型内存布局,这是CLR用来区分如何分配实例的内存的关键.还有类型标记(反射和JIT编译时会使用类型标记来快速的查找类型).

 

下面来看看Struct和Class有什么不同?

 1 TypeDef #2 (02000004) 类型标记码
 2 -------------------------------------------------------
 3     TypDefName: TestDemo1.Type.TestValueType1  (02000004)
 4     Flags     : [Public[SequentialLayout] //类型字段加载方式  [Class] [Sealed] [AnsiClass] [BeforeFieldInit
 5     Extends   : 01000002 [TypeRef] System.ValueType
 6     Layout    : Packing:0Size:1
 7 
 8 TypeRef #2 (01000002)
 9 -------------------------------------------------------
10 Token:             0x01000002
11 ResolutionScope:   0x23000001
12 TypeRefName:       System.ValueType

  值类型都继承自ValueType,当然ValueType也最终继承自Object,在这里大家可以看到它的Flags中也是Class,而且还是Sealed.还有一个需要注意的是Layout中,它的Size竟然是1,我们并没有定义任何的成员阿?这里的1笔者将会在后面给出答案.

我们再来看看接口有什么不同

1 TypeDef #3 (02000005)//类型标记码
2 -------------------------------------------------------
3     TypDefName: TestDemo1.Type.ITestInterface1  (02000005)
4     Flags     : [Public] [AutoLayout] [Interface] [Abstract] [AnsiClass]  
5     Extends   : 01000000 [TypeRef] //父类不存在

  可以看到接口不存在任何父类,它只是一个纯抽象的契约而已,很多书上都说任何类型都最终继承自Object是不严谨的.那么为什么接口不继承自Object呢,简单来说由于Object已实现的方法,如果接口继承自它,那么接口也就有了这些虚方法和受保护的方法,虚方法可以重写,那么就破坏了接口做为契约的本质.在实现了接口的所有类型的方法表中,将开辟两个Tostring,两个GetHashcode等的方法槽造成你调用方法时产生二义性,并且还会大量的占用内存(虽然一个方法槽只有4个字节,但是架不住类型多).你或许会说两个方法槽最终可以指向同一个方法地址,Ok.这样说两个相同的方法有同一种实现,即占用了内存,还什么用都没有,为什么要这样做呢?

  OK,讨论完接口我们来看看委托,这里我们只是为了解一下委托的类型,关于委托笔者会单独写一篇文章来探索它.

1 TypeDef #4 (02000006)//类型标记码
2 -------------------------------------------------------
3     TypDefName: TestDemo1.Type.TestDelegate1  (02000006)
4     Flags     : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass]  
5     Extends   : 01000003 [TypeRef] System.MulticastDelegate //父类

委托在实际就是一个类,继承自(MulticastDelegate,它又继承自Delegate,而最终也是Object的孩子),并且和值类型一样都是Sealed.表示它们不可以被继承,至于为什么不可以被继承,笔者将会在后续的文章中介绍. 我们在这里讨论了Class,Interface,struct,Delegete 发现他们的类型标记都是0200000X,都是用TypeDef来定义类型,用TypeRef来表述父类,并且有足够的信息来描述你写的C#代码.而且除了Interface,全部都被定义为Class.那么当CLR来加载这些元数据时,靠什么来区分你所定义的Struct和Class呢?CLR如何快速去查找类型呢?

dll中的数据到底是什么样子的,当CLR加载一个DLL乃至加载一个类型到物理内存的时候如何查找类型?

在编译好的dll中,实际上是由很多张表,以及一些特定的string堆,Guid堆,BLOB堆来组成的元数据,我们先来看看TypeDef表.你所定义的类型都被放在TypeDef表中,这个表你可以想象成如同二维的数据库表一样,有Token类型标记码作为主键,还可以查找到它的父类(Extends)的类型标记码.以及类型到底是Class,Valuetype,Interface(图中Flags的注释中的C,I,V)前两个是根据他们的父类来识别,而Interface有关键字来表示,参照上面的元数据.

 

 


  我们现在来看看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 0x023cb928
Name:        TestDemo1.Type.TestValueType1
MethodTable: 00313894
EEClass:     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堆和栈),反射以及垃圾回收器,异常管理等内容.

 

 

 


 

作者:徐磊

出处:http://www.cnblogs.com/xugao918/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。


 

 

posted on 2011-09-19 09:26 徐 磊 阅读(3819) 评论(91) 编辑 收藏

FeedBack:
#1楼 2011-09-19 09:46 Artech      
不错,推荐!
 回复 引用 查看   
#2楼[楼主2011-09-19 09:51 徐 磊      
@Artech
谢谢!

 回复 引用 查看   
#3楼 2011-09-19 10:02 Andy晨      
好文章,值得深入研究
 回复 引用 查看   
#4楼 2011-09-19 10:21 lipan      
先顶在看
 回复 引用 查看   
#5楼 2011-09-19 11:18 kkun      
上班时间不敢光明正大的看,先推荐了
 回复 引用 查看   
#6楼 2011-09-19 14:29 xiao_p      
如今在cnblogs还能看到这样的文章,不容易啊
 回复 引用 查看   
#7楼[楼主2011-09-19 14:39 徐 磊      
引用xiao_p:如今在cnblogs还能看到这样的文章,不容易啊

3年没有写过Blog了,不知道如今的博客园和3年前有什么区别.

 回复 引用 查看   
分析的很底层啊,学习了。
跟着楼主学了,话说以我小菜的水平,能看懂一部分已经感觉庆幸了

 回复 引用 查看   
#9楼[楼主2011-09-19 16:28 徐 磊      
引用丫头小静(Cathy):
分析的很底层啊,学习了。
跟着楼主学了,话说以我小菜的水平,能看懂一部分已经感觉庆幸了

呵呵,慢慢来.我这只是.net做的比较久而已,共同学习,共同成长

 回复 引用 查看   
#10楼 2011-09-19 17:50 天空海阔      
弱弱问下,SOS是什么?
 回复 引用 查看   
#11楼[楼主2011-09-19 18:00 徐 磊      
引用天空海阔:弱弱问下,SOS是什么?

http://msdn.microsoft.com/zh-cn/library/bb190764.aspx
看这个吧

 回复 引用 查看   
#12楼 2011-09-19 18:09 彪悍人生...      
留个记号,现在还是菜鸟,等我成长为老鸟了再来看...
 回复 引用 查看   
#13楼[楼主2011-09-19 18:20 徐 磊      
@彪悍人生...
呵呵...

 回复 引用 查看   
#14楼 2011-09-19 19:08 乱舞春秋      
强烈关注中,这个真给力,好久没有看到这么深入研究的好文章了!
 回复 引用 查看   
#15楼 2011-09-19 19:12 DiggingDeeply      
讲了个metedata
SOS 是strike on 啥来着,忘记了,

 回复 引用 查看   
#16楼 2011-09-19 19:15 LoveJenny      
彪悍的人生,彪悍的程序员,支持
 回复 引用 查看   
#17楼 2011-09-19 19:24 TabEnter      
@DiggingDeeply
son of strike

 回复 引用 查看   
#18楼 2011-09-19 22:26 听说读写      
看CLR via c#和msdn可以得到完整的。net程序集和执行模型分析
 回复 引用 查看   
#19楼 2011-09-20 00:53 lcs-帅      
好爽哦。感觉好久没有看到如此好文了。
 回复 引用 查看   
#20楼[楼主2011-09-20 07:33 徐 磊      
@听说读写
CLR VIA 我 也看过,但是有一些问题没有找到答案,比如字段的分布,接口虚表和方法虚指派。以及栈上的内存结构,数据类型转换等,都是点到为止。

 回复 引用 查看   
#21楼[楼主2011-09-20 07:33 徐 磊      
@lcs-帅
谢谢关注

 回复 引用 查看   
#22楼[楼主2011-09-20 07:34 徐 磊      
@乱舞春秋
同勉

 回复 引用 查看   
#23楼[楼主2011-09-20 07:34 徐 磊      
@LoveJenny
彪悍的回复

 回复 引用 查看   
#24楼 2011-09-20 09:08 空逸云      
不错,推荐。这里有几点不明白的地方,麻烦LZ帮解释下
引用mdToken: ed13f79202000003 //红色部分是Dll被加载到虚拟内存空间的地址

引用类型标记码(2000003)

不知道ClassLoader查找类型是根据这个类型标记码么?前面这个红色部分,我还是不太理解虚拟内存空间地址这个概念,能理解为就是该Dll被加载到内存中的地址么?

 回复 引用 查看   
#25楼[楼主2011-09-20 09:15 徐 磊      
@空逸云
ClassLoader在加载类型时,是根据类型标记的
红色部分就是dll在虚拟内存中的标识

 回复 引用 查看   
#26楼 2011-09-20 09:44 mzfhhhh      
请教楼主
TestObjectType1 obj = new TestObjectType1();
执行前 //线程栈地址 0x0022eba8 = 0x00000000
执行后 0x0046ec98 = 0x0240b928

文中 0x0022eba8 是线程栈地址
0x0240b928 GC堆中TestObjectType1实例地址

那这个0x0046ec98 是什么地址?

 回复 引用 查看   
#27楼[楼主2011-09-20 09:48 徐 磊      
@mzfhhhh
谢谢提醒,这是我执行了两次Dubug的结果.不好意思,写着写着忘记了,马上修改

 回复 引用 查看   
#28楼 2011-09-20 10:02 听说读写      
@徐 磊
引用徐 磊:
@听说读写
CLR VIA 我 也看过,但是有一些问题没有找到答案,比如字段的分布,接口虚表和方法虚指派。以及栈上的内存结构,数据类型转换等,都是点到为止。


字段和内存布局有关, 里面有提到(CLI没有定义字段的内存分布规范,即使现在是顺序的,但是不保证以后是顺序的)
方法都在方法表里面,当然根据继承,override和new 方法会定义在不同的类型上,同样的,调用方法的IL指令也不一样
你说的是哪个栈? 执行栈和其他语言中的执行栈一样(x86),所有这个体系的语言到了这个级别都长一样
数据类型转换,只要没有box,unbox 数据本身的内存都不变,只变类型指针(某些特殊类型可能不一样)

 回复 引用 查看   
#29楼[楼主2011-09-20 10:20 徐 磊      
@听说读写
呵呵,那我问你,接口的方法,是如何多态的?在方法表中的布局是什么样的?
比如接口 IA,有两个方法 a,b
那么所有的使用 IA.a 和 IA.b 的方法,如何找到类型实例?如何定位到方法?他们的方法偏移量 在IL中是一模一样的,一个是1,一个是2
你的所有实现的类型的方法表中,这些方法的偏移是不可能一样的
这样CLR VIA 里有吗?

关于栈,请问栈上有什么,在类型转换时,如果类型不一样,为什么会报错?你怎么知道栈上是什么类型?

 回复 引用 查看   
#30楼 2011-09-20 10:38 听说读写      
引用徐 磊:
@听说读写
呵呵,那我问你,接口的方法,是如何多态的?在方法表中的布局是什么样的?
比如接口 IA,有两个方法 a,b
那么所有的使用 IA.a 和 IA.b 的方法,如何找到类型实例?如何定位到方法?他们的方法偏移量 在IL中是一模一样的,一个是1,一个是2
你的所有实现的类型的方法表中,这些方法的偏移是不可能一样的
这样CLR VIA 里有吗?

关于栈,请问栈上有什么,在类型转换时,如果类型不一样,为什么会报错?你怎么知道栈上是什么类型?



让我会先回答你第一个问题吧
IL通过 CALL 和CALLvirt 两个指令实现实现两种不同的调用方法
其中后者是运行时才检查类型的,因此需要多一个实例的参数,根据实例参数知道实例类型,然后根据继承链(是否有override等)由下至上找到真正需要执行的方法,这里的类继承和接口继承的行为是一致的

看到你的问题我觉得你可能不理解,类型定义上(ie:typeof(classA))是包含了方法定义的.实际执行中:在第一次遇到这个方法的时候执行动态编译,并加载到方法表和缓存中,这时候才有物理地址的偏移量(可以查看加载和执行模型的相关内容)

说到这里,我也反问你一个简单的问题,请问值类型继承接口,那么调用时的行为是怎样的呢?

另外我觉得你看别的人回复要认真点,
我的原话是:看CLR via c#和msdn可以得到完整的。net程序集和执行模型分析
你就直接把MSDN给省略掉了

 回复 引用 查看   
#31楼 2011-09-20 10:42 空逸云      
@徐 磊
类型转换当中,内存始终没有发生变化,变化的是IL,IL来说明到底我想转到什么类型,对于类型转换也仅仅是一个isinst或castclass指令,具体的类型匹配是通过底层的一个类型兼容性测试,如果不兼容则抛出异常,这个兼容性是如何实现的,就不得而知,不过可以猜想是根据TypeHandler来判断的。

 回复 引用 查看   
#32楼[楼主2011-09-20 10:46 徐 磊      
引用听说读写:
你没有回答我接口方法虚指派的问题.
你的问题:值类型,调用方法:如果是直接使用值类型.方法调用,不多态,也不需要装箱(因为第一个参数被指定为值类型实例指针)
如果接口调用,装箱(因为接口编译时需要引用指针,而且不装箱没有类型指针,无法多态)并且虚调用(多态).至于怎么样多态的,可以请你来回答.

还有,偏移量是IL编译时指定的.因为它是一个针对方发表的固定整形的地址差值.不是JIT的时候才产生的.
 回复 引用 查看   
#33楼[楼主2011-09-20 10:51 徐 磊      
引用空逸云:
@徐 磊
类型转换当中,内存始终没有发生变化,变化的是IL,IL来说明到底我想转到什么类型,对于类型转换也仅仅是一个isinst或castclass指令,具体的类型匹配是通过底层的一个类型兼容性测试,如果不兼容则抛出异常,这个兼容性是如何实现的,就不得而知,不过可以猜想是根据TypeHandler来判断的。

呵呵,没错!实际在栈上,并不只有引用指针或值这么简单,.net的栈中的每一项,实际上是个槽,有两部分内容 类型签名--值或引用地址.
这样就可以检查引用的类型和栈上声明的类型,是否兼容
关于兼容性的检查是通过EEclass,就是文中提到的那个.EEclass是类型树上的一个节点,通过它可以遍历整个继承系统(你的类的,不是.net的整个)

 回复 引用 查看   
#34楼 2011-09-20 10:55 听说读写      
接口方法虚指派? 这是什么术语? google了一下也没有这个术语,还请赐教
你刚才也提到了类型指针,有了类型指针,再有相关类型的类型定义,那么就可以在运行时决定真正调用的方法,

 回复 引用 查看   
#35楼 2011-09-20 10:57 听说读写      
引用空逸云:
@徐 磊
类型转换当中,内存始终没有发生变化,变化的是IL,IL来说明到底我想转到什么类型,对于类型转换也仅仅是一个isinst或castclass指令,具体的类型匹配是通过底层的一个类型兼容性测试,如果不兼容则抛出异常,这个兼容性是如何实现的,就不得而知,不过可以猜想是根据TypeHandler来判断的。


在堆中确实是没有变化的, 变化的只是类型指针,就像是一个标识, 标识这个东西是A还是B
在栈上的会复杂点, IL内置了很多指令来做一些基本类型的转换
例如conv.i8 conv.i4 (不知道有没有打错)

 回复 引用 查看   
#36楼[楼主2011-09-20 11:01 徐 磊      
@听说读写
接口虚表.用来指派多态的接口方法的指派...
不只那么简单
就是我说的那个例子,IA.a IA.b 当你的实例转换为接口调用这些方法,那么产生的IL是相同的,你可以IL反编译看看.
虚方法的指派是根据方法表+偏移量来执行的,这些就会产生你的接口的偏移和实际类型的偏移不一样.
你说的MSDN的执行方式我也看过,里面有讲到,但不是那么清晰,没有说明怎么从IL加载到CLR,CLR执行时如何查找.

 回复 引用 查看   
#37楼 2011-09-20 11:08 听说读写      
那让我上一段代码,看看我们的理解是不是存在gap
// 声明
public interface IA
{
void TestA();
}
public class B : IA
{
public virtual void TestA()
{
Console.WriteLine(1);
}
}
public class C : B
{
public new void TestA()
{
Console.WriteLine(2);
}
}
//执行
IA i = new C();
B b = new C();
C c = new C();
i.TestA();
b.TestA();
c.TestA();
//IL, 删除了不重要的部分
IL_0014: callvirt instance void IA::TestA()
IL_001b: callvirt instance void B::TestA()
IL_0022: callvirt instance void C::TestA()

请问是否指的是以上代码的情况

 回复 引用 查看   
#38楼[楼主2011-09-20 11:11 徐 磊      
@听说读写
这样不明显
你可以使用 A,B分别实现你定义的接口
当然 A和B要有一些自己的方法,比如A有4个(Object继承下来的也行),B有7个.
当你实现接口的时候,接口的方法的偏移量就分别:
A在5,6
B在8,9

 回复 引用 查看   
#39楼[楼主2011-09-20 11:21 徐 磊      
@听说读写
当时我看完CLR VIA的时候想了很久这个问题
而且翻遍了CLR VIA没找到
所以我写这系列文章,只是为了把IL-clr,以及为什么这样设计,它的好处是什么,提出来,大家分享.也有很多我不明白为什么要这样做的地方,希望像你这样有探索精神的程序员,可以和我一起探讨.

 回复 引用 查看   
#40楼 2011-09-20 11:22 听说读写      
你这里的偏移量是否指的是,特定方法在方法表中相对开始位置的偏移量?
据我所知:子类继承父类时,虚方法的方法布局层次结构是不变的
也就是说父类的方法偏移是多少,子类也是多少
例如实际类型是ClassC 但是调用了B.MethodA()
B中的偏移是x, C.MethodA的偏移也是x
推论1:所有父类的虚方法都会被继承下来
推论2: 每个类型的方法前4个是Object对象的4个虚方法 (我没验证过)

无论是IL中的CALL 还是CALLvirt, 最终都会经过JIT后生成的物理地址偏移量, 至此这个方法表才是完善的, 某个特定类型上的特定方法才指向真正执行的地址. (也就是存的是方法的执行地址)

 回复 引用 查看   
#41楼[楼主2011-09-20 11:26 徐 磊      
@听说读写
对,就是方法在方法表中相对开始位置的偏移量
子类继承父类时,虚方法的方法布局层次结构是不变的 这个也对,没有任何问题.
关键在于接口的时候,就会不一样了
.net不支持多继承,也有这方面数据结构(CLR的类型结构布局)设计的问题.

 回复 引用 查看   
#42楼[楼主2011-09-20 11:26 徐 磊      
引用听说读写:
你这里的偏移量是否指的是,特定方法在方法表中相对开始位置的偏移量?
据我所知:子类继承父类时,虚方法的方法布局层次结构是不变的
也就是说父类的方法偏移是多少,子类也是多少
例如实际类型是ClassC 但是调用了B.MethodA()
B中的偏移是x, C.MethodA的偏移也是x
推论1:所有父类的虚方法都会被继承下来
推论2: 每个类型的方法槽表前4个方法是Object对象的4个虚方法 (我没验证过)

无论是IL中的CALL 还是CALLvirt, 最终都会经过JIT后生成的物理地址偏移量, 至此这个方法表才是完善的, 某个特定类型上的特定方法才指向真正执行的地址. (也...

和你讨论很开心啊,像回到了3年前的BlogCN

 回复 引用 查看   
#43楼 2011-09-20 11:37 空逸云      
@听说读写
引用听说读写:
在堆中确实是没有变化的, 变化的只是类型指针,就像是一个标识, 标识这个东西是A还是B
在栈上的会复杂点, IL内置了很多指令来做一些基本类型的转换
例如conv.i8 conv.i4 (不知道有没有打错)

不对喔。一旦实例化了一个对象。那么它的TypeHandler就已经确定下来了。这个不会因为类型的转换而变化。转换类型后的实例调用方法是根据IL中转换类型的方法表调用。所以说,整个类型转换中,变化的仅仅只有IL,GCHeap中内存是不发生变化的。

 回复 引用 查看   
#44楼 2011-09-20 11:40 听说读写      
@徐 磊
同感~

 回复 引用 查看   
#45楼 2011-09-20 11:40 空逸云      
@徐 磊
引用实际在栈上,并不只有引用指针或值这么简单,.net的栈中的每一项,实际上是个槽,有两部分内容 类型签名--值或引用地址
这个我不确定,不过我印象中看过的文章中说栈中仅有值或引用地址,这个类型签名我没了解过,一直很怀疑是不是栈上有类型的信息,通过SOS也没发现什么蛛丝马迹,不知道LZ怎么确定是两个槽的。能否提供具体的SOS查看方法。

 回复 引用 查看   
#46楼 2011-09-20 11:52 听说读写      
首先,从IL来看
多继承的方法每个方法都有自己的slot

.method private final hidebysig newslot virtual
instance void IB.TestA () cil managed

.method public final hidebysig newslot virtual
instance void TestB () cil managed

.method private final hidebysig newslot virtual
instance void IA.TestA () cil managed

然后,
显示实现接口的方法 例如IA.TestA() IB.TestA() 是不可以被override!!
那么也就是说 他没有被继承以后内存布局改变的问题 (我觉得这是关键点)

所以在当前的class上 他可以随意做内存布局 而不会影响到子类 (同时,我想他也不会继承IA,IB的内存布局[凡是显示实现接口])

部分内容只是我的猜测

 回复 引用 查看   
#47楼 2011-09-20 12:04 听说读写      
@空逸云
不好意思 我表述的有问题
我的理解是,表示内容部分的(LZ的GCHeap中的东西 不改变)
这点我们是一致的

这里我说的类型指针(这个词语使用不当) 应为 指向该内容的指针类型
例如object a= (class)c;
例如string c= (class)c;

另外我同意你的只是改变IL中的指令 (实例参数还是同一个)~




 回复 引用 查看   
#48楼 2011-09-20 12:11 听说读写      
BTW: 感谢大家的分享
 回复 引用 查看   
#49楼[楼主2011-09-20 12:27 徐 磊      
@听说读写
类型是不会变化,但是只靠方法表,你是无法确定该运行那个方法的
因为IL中是一样的方法偏移,关键就在这里,对于接口来说不存在,7,8这个偏移量的方法.
这个实例是动态类型,在编译时无法确定,所以IL中表示,都是方法的偏移量,1,2.
不知道我这样说,你明白我的意思了吗?

 回复 引用 查看   
#50楼[楼主2011-09-20 12:31 徐 磊      
引用听说读写:
@空逸云
不好意思 我表述的有问题
我的理解是,表示内容部分的(LZ的GCHeap中的东西 不改变)
这点我们是一致的

这里我说的类型指针(这个词语使用不当) 应为 指向该内容的指针类型
例如object a= (class)c;
例如string c= (class)c;

另外我同意你的只是改变IL中的指令 (实例参数还是同一个)~




你可以看我最上面的那张图 在LoadHeap的ObjectType1的 interface VirMap 它指向了一个Virtual Map的接口虚表,这个就是它可以完成多接口继承,以及接口方法虚指派(多态)的秘密.
我后面的文章会阐述这个问题,现在没图,没真相不好解释.

 回复 引用 查看   
#51楼[楼主2011-09-20 12:34 徐 磊      
@空逸云
兄弟是一个槽....不是两个槽
槽里记录了两部分内容.后面我的文章会详细的描述.用sos确实看不到.

 回复 引用 查看   
#52楼[楼主2011-09-20 12:40 徐 磊      
@听说读写
不是显示实现
可能我那样写,让你误会
而是
IA a1=new 实现了IA的类1(); 类1其中总共有7个方法(包括接口的实现)
IA a2=new 实现了IA的类2(); 类2其中总共有9个方法
a1.a();
a2.a();
很明显两个a的实现方法不在同一个偏移量上.
这时候你看IL都是同样的,a方法对于接口类型来说偏移量为1.

光用方发表肯定不能解决这个问题.

 回复 引用 查看   
#53楼 2011-09-20 13:46 空逸云      
引用徐 磊:
@空逸云
兄弟是一个槽....不是两个槽
槽里记录了两部分内容.后面我的文章会详细的描述.用sos确实看不到.

哇。。你说的我好心动啊。。哈哈。。求资料。。求资料!!

 回复 引用 查看   
#54楼 2011-09-20 14:03 听说读写      
引用徐 磊:
@空逸云
兄弟是一个槽....不是两个槽
槽里记录了两部分内容.后面我的文章会详细的描述.用sos确实看不到.

cool,等你的文章~~

 回复 引用 查看   
#55楼 2011-09-20 14:06 听说读写      
子类继承父类时,虚方法的方法布局层次结构是不变的
这个能否运用在接口和子类上?

 回复 引用 查看   
#56楼[楼主2011-09-20 14:10 徐 磊      

子类继承父类时,虚方法的方法布局层次结构是不变的
这个能否运用在接口和子类上?

不能的,因为对于实现接口的类来说,它的方法槽数是不固定的

 回复 引用 查看   
#57楼 2011-09-20 14:12 空逸云      
说到方法表,有一个问题我一直疑惑了很久.类型的方法表中有两个表,方法表和方法槽表,方法表代表这个类声明(重写)了什么方法,而方法槽表则说明这个类有多少方法(我的理解),当我们定义一个新方法的时候,会有newslot这个标记,如果没有这个标记,则认为是新方法,若有vistual标记且无newslot标记则为新虚方法,这个槽表上的槽存放的是方法表上该方法的偏移量。这样每次方法的调用是先路由方法槽表然后定位到方法表,只是这个父类子类的方法表是继承的关系还是复制到关系,也就是说子类中的方法表中是否有父类的方法(非重写),我更偏向于认为是继承关系,但是继承的话却又是怎样找到父类的方法,毕竟槽表上仅有方法表的偏移量。。
 回复 引用 查看   
#58楼[楼主2011-09-20 14:13 徐 磊      
@空逸云
呵呵,后面的文章,我会像一些办法证明线程栈上的东西.

 回复 引用 查看   
#59楼[楼主2011-09-20 14:31 徐 磊      
@空逸云
你的理解不太正确,只有一个方法表,方法表的每一项是一个槽.
子类继承父类(不包括接口)永远是 从上到下 分别是父类的虚方法,自己的新的虚方法,然后是非虚方法,.net单继承所以这个数据结构是没有问题的.
也就是说Object的4个虚方法,在任何类型的方法表中永远是1,2,3,4的排位,位置不变.不管你有没有重写都会复制到子类里面,如果不重写只是方法的具体实现指向同一个方法描述.
对虚方法的判定就是Virtual关键字.

 回复 引用 查看   
#60楼 2011-09-20 14:40 空逸云      
@徐 磊
http://www.microsoft.com/china/MSDN/library/netFramework/netframework/JITCompiler.mspx?mfr=true
这里就说到了这两张表,这些概念的确是很混乱,而且对于EEClass也很模糊,有文章说到EEClass其实也是方法表,这就更糊涂了。希望通过分析内存或其他更直观的方式看到结果,而非仅仅的字面描述。期待你的下一篇文章

 回复 引用 查看   
#61楼 2011-09-20 14:41 空逸云      
http://i.msdn.microsoft.com/cc163791.fig09(en-us).gif
这张图中很明确的看到的确是有方法表和方法槽表两张表。

 回复 引用 查看   
#62楼 2011-09-20 15:05 南京.王清培      
这文章写的惊世骇俗啊,扫遍各大IT网站真的很好能看见这样深入讨论.NET的文章。都说.NET简单,应该把那些说.NET简单的人拉过来看看。啥叫技术 呵呵 顶顶
 回复 引用 查看   
#63楼 2011-09-20 15:07 南京.王清培      
真心希望楼主能出点关于深入.NET的书籍,让我们后辈能有机会学习。
 回复 引用 查看   
#64楼[楼主2011-09-20 15:15 徐 磊      
@空逸云
那是方法描述表,方法表记录了方法的地址,方法描述表才会有方法的具体IL
也就是说,两个类型的方法表里面的内容可以指向同一个方法,例如 Tostring()

 回复 引用 查看   
#65楼[楼主2011-09-20 15:23 徐 磊      
引用南京.王清培:真心希望楼主能出点关于深入.NET的书籍,让我们后辈能有机会学习。

呵呵...我只是做.net比较长时间,还出不了书.

 回复 引用 查看   
#66楼[楼主2011-09-20 15:26 徐 磊      
@空逸云
其实方法表的值是指向方法槽表,也就是说只有方法槽表里面才有方法的列表.
一般都是直接说方法表,就是指的这个槽表,你看我的图就明白了,我的图上没有画具体的方法描述表(有IL的那个).

 回复 引用 查看   
#67楼 2011-09-20 17:24 阿水      
写的很深入呀!
 回复 引用 查看   
#68楼[楼主2011-09-20 17:39 徐 磊      
@阿水
呵呵,随便写写

 回复 引用 查看   
#69楼 2011-09-20 21:57 听说读写      
引用徐 磊:
@听说读写
不是显示实现
可能我那样写,让你误会
而是
IA a1=new 实现了IA的类1(); 类1其中总共有7个方法(包括接口的实现)
IA a2=new 实现了IA的类2(); 类2其中总共有9个方法
a1.a();
a2.a();
很明显两个a的实现方法不在同一个偏移量上.
这时候你看IL都是同样的,a方法对于接口类型来说偏移量为1.

光用方发表肯定不能解决这个问题.


hi,我注意到。net里面有这么一个attribute

ReuseSlot Indicates that the method will reuse an existing slot in the vtable. This is the default behavior.

我写了一段关于测试两个类继承一个接口的问题
因为代码和测试结果比较长,请查看这里:http://www.cnblogs.com/PurpleTide/archive/2011/09/20/2182877.html

两个类的继承与接口的方法的ReuseSlot 的值都是true



 回复 引用 查看   
#70楼 2011-09-20 22:03 听说读写      
此外显示实现接口
引用徐 磊:
@空逸云
你的理解不太正确,只有一个方法表,方法表的每一项是一个槽.
子类继承父类(不包括接口)永远是 从上到下 分别是父类的虚方法,自己的新的虚方法,然后是非虚方法,.net单继承所以这个数据结构是没有问题的.
也就是说Object的4个虚方法,在任何类型的方法表中永远是1,2,3,4的排位,位置不变.不管你有没有重写都会复制到子类里面,如果不重写只是方法的具体实现指向同一个方法描述.
对虚方法的判定就是Virtual关键字.


我同意继承以后, 虚方法的顺序和位置不变,因此可以保证所有子类的虚方法位置不变,这点对于普通接口继承和类继承来说应该是一致的

此外通过测试可以发现 唯一能进行多继承的多接口继承, (1:实现不能被再次继承, 2:该方法定义根本就不在这个类里面)
例如以下代码

public interface IA
{
void Test1();
}
public class A : IA
{
void IA.Test1() { Console.WriteLine(1); }
}

通过查看 typeof(A).GetMembers() 的内容
Test1方法没有被定义在 typeof(A) 里面
这应该是。net特别处理的一种情况?

 回复 引用 查看   
#71楼 2011-09-20 23:42 听说读写      
此外关于多继承
有一个额外的Attribute是关于这个的
VtableLayoutMask

我想这就是 空逸云 说的同一个slot 不同的布局的问题,
目前还没有获得更进一步的资料

 回复 引用 查看   
#72楼[楼主2011-09-21 09:48 徐 磊      
我同意继承以后, 虚方法的顺序和位置不变,因此可以保证所有子类的虚方法位置不变,这点对于普通接口继承和类继承来说应该是一致的

每个类实现不同数量的接口,每个类的方法数量根本就不一样,如何保证方法的偏移量是一样的,你可以想一下,根本不可能槽数是一样的,对于接口来说,比方说他只有一个方法,那么偏移量就是1 ,你的所有类的方法偏移量为1的方法,必然是Object中的方法,如何多态呢?

 回复 引用 查看   
#73楼[楼主2011-09-21 09:49 徐 磊      
@听说读写
引用
'我同意继承以后, 虚方法的顺序和位置不变,因此可以保证所有子类的虚方法位置不变,这点对于普通接口继承和类继承来说应该是一致的'.

每个类实现不同数量的接口,每个类的方法数量根本就不一样,如何保证方法的偏移量是一样的,你可以想一下,根本不可能槽数是一样的,对于接口来说,比方说他只有一个方法,那么偏移量就是1 ,你的所有类的方法偏移量为1的方法,必然是Object中的方法,如何多态呢?

 回复 引用 查看   
#74楼[楼主2011-09-21 09:51 徐 磊      
@听说读写
void IA.Test1() { Console.WriteLine(1); }
你这样显示实现接口,实现的方法根本不属于这个实例,虽说属于这个类,但是却无法访问的,你GetMember肯定没有的.

 回复 引用 查看   
#75楼 2011-09-21 09:56 听说读写      
引用徐 磊:
@听说读写
引用
'我同意继承以后, 虚方法的顺序和位置不变,因此可以保证所有子类的虚方法位置不变,这点对于普通接口继承和类继承来说应该是一致的'.

每个类实现不同数量的接口,每个类的方法数量根本就不一样,如何保证方法的偏移量是一样的,你可以想一下,根本不可能槽数是一样的,对于接口来说,比方说他只有一个方法,那么偏移量就是1 ,你的所有类的方法偏移量为1的方法,必然是Object中的方法,如何多态呢?



分享一个资料
http://msdn.microsoft.com/en-us/magazine/cc163791.aspx#S10

CLR对此做了一些特殊的处理

 回复 引用 查看   
#76楼[楼主2011-09-21 10:17 徐 磊      
@听说读写
这个我看过,文章中讲了接口部分的内容,但是不够详细
你看看Virtual Dispatch这部分.

 回复 引用 查看   
#77楼 2011-09-23 15:21 听说读写      
最近比较忙,
关于接口和类实现时候的方法入口,那个文章里面有提到
(不过在实践中 2.0 4.0 的CLR 排列顺序和1.1不一样)

Interface Vtable Map and Interface Map
At offset 12 in the MethodTable is an important pointer, the IVMap. As shown in Figure 9, IVMap points to an AppDomain-level mapping table that is indexed by a process-level interface ID. The interface ID is generated when the interface type is first loaded. Each interface implementation will have an entry in IVMap. <b>If MyInterface1 is implemented by two classes, there will be two entries in the IVMap table. The entry will point back to the beginning of the sub-table embedded within the MyClass method table, as shown in Figure 9</b>. This is the reference with which the interface-based method dispatching occurs. IVMap is created based on the Interface Map information embedded within the method table. Interface Map is created based on the metadata of the class during the MethodTable layout process. Once typeloading is complete, only IVMap is used in method dispatching.

然后直接用SOS 还有内存窗口去查看验证

 回复 引用 查看   
#78楼[楼主2011-09-23 15:45 徐 磊      
@听说读写
呵呵,对了,就是我图上的Interface Virtual Map
他记录了接口标识和方法表首地址(在MethodTable中的起始地址),这样就可以完成我们上面讨论过的方法偏移始终是一样的也可以多态的功能了.
欢迎你常来啊,我们可以讨论很多东西.
不过我的Blog会更新很慢,大概一周一篇吧...

 回复 引用 查看   
楼主能这么深入的研究CLR,
不知道用.net几年了?

 回复 引用 查看   
#80楼[楼主2011-10-05 22:00 徐 磊      
@孤独的设计师
呵呵,快8年了

 回复 引用 查看   
#81楼 2011-10-06 15:45 DotNetProgramer      
好文章
 回复 引用 查看   
#82楼 2011-10-08 15:39 ryouga      
LOAD堆里存的是类类型是吧?另外GC堆和大对象堆是不同的概念吗?我看书上讲的大对象是存放在托管堆的2代内存里面,翻译的机制跟其他对象一样只是在压缩堆时不移动大对象的地址。
 回复 引用 查看   
#83楼[楼主2011-10-08 16:57 徐 磊      
@ryouga
Load存放的是类型的信息.可以理解为模板,通过这个模板生成实例(不是静态类).
你所谓的2代内存是什么意思?
你是说GC的2代?

 回复 引用 查看   
#84楼[楼主2011-10-08 16:58 徐 磊      
@DotNetProgramer
谢谢

 回复 引用 查看   
#85楼 2011-10-08 21:53 秋忆      
太精彩了,园子好久没有这样的强文了。继续关注学习!
 回复 引用 查看   
#86楼[楼主2011-10-09 10:37 徐 磊      
@秋忆
谢谢关注

 回复 引用 查看   
#87楼 2011-10-31 09:42 阿水      
很精彩
 回复 引用 查看   
#88楼[楼主2011-10-31 10:39 徐 磊      
引用阿水:很精彩

谢谢

 回复 引用 查看   
#89楼 2011-12-23 10:19 The Game      
牛了个逼了!知道我是谁不。
 回复 引用 查看   
#90楼 2012-01-09 19:08 Jester Zhu      
加关注了,文章很深入。
我要多学习,多了解,多研究。

 回复 引用 查看   
#91楼 2012-02-21 10:00 吾爱孟夫子      
@ryouga

GC堆实际上有两个堆:大对象堆(LOH)和小对象堆(SOH),大对象分配在大对象堆中。小对象的堆的垃圾回收是要移动内存的(移动后小对象堆中的对象又是连续排列的)。而大对象堆是不移动内存的,(为了效率,但这样就会产生碎片)。

为了加快效率,小对象堆上的垃圾回收是分代的。回收0代时,1和2代是不回收的。回收1代时,0代也一同回收。同样回收2代时,0和1代也一并回收。

那么,什么时候回收大对象堆呢,那就是在小对象堆上进行2代回收时。这就是为什么说大对象是2代对象的原因。

 回复 引用 查看