无疑,一个Object在CLR中的逻辑结构是相当复杂的。
前段时间,写了一篇CLR探索系列:System.Object内存布局模型及实现研究,侧重从System.Object这个基本类的基本内存布局,实现和结构来研究了下。这是远远不够的。今天就从如何存储一个Object中的Field,Method等信息,这些信息的逻辑组织方式和存储的逻辑结构。
废话不多说,看看就知道了:
首先,给一个图:
这个图,显示了一个Object的MethodTable,EEClass和MethodDescChunk之间的大致关系。至于一个整体的Object的实现的逻辑结构模型图,结构比较复杂,不好画,google了半天也没找到个现成的,就用这个凑合了。
通常,在内存中,对一个instance的ref的这个地址,其实是指向这个instance的MethodTable的。这个内容,我在“System.Object这个基本类的基本内存布局,实现和结构”这篇文章里面也有说,具体是如何实现的可以参照那篇文章。
老规矩,截取点MethdTable里面重要的属性,方法和结构体,总共一个MethodTable的实现,就大概2500多行,(太长了?这仅仅是定义部分),只能截取表示如下了:
class MethodTable
{
// Low WORD is component size for array and string types, zero otherwise
DWORD m_wFlags;
// Base size of instance of this class when allocated on the heap
DWORD m_BaseSize;
//Nmuber of Method
WORD m_wNumMethods;
//Number of Vitural Methods
WORD m_wNumVirtuals;
WORD m_wNumInterfaces;
// LoaderModule. It is equal to the ZapModule in ngened images
PTR_Module m_pLoaderModule;
PTR_MethodTable m_pParentMethodTable;
// This is the way to create a new method table. Don't try calling new directly.
// Mehtod tables are also created in array.cpp where an EEClass
// and MethodTable are created in one fell swoop. I see no rationale basis for
// such an approach.
// Even worse almost exactly the same stuff is duplicated in DynamicMethod.cpp.
// Worse still almost exactly the same stuff is duplicated in generics.cpp.
//MethodTable的New函数,MethodTalbe不能直接New一个
static MethodTable * AllocateNewMT(EEClass *pClass,
DWORD dwVtableSlots,
DWORD dwGCSize,
DWORD dwNumInterfaces,
DWORD numGenericArgs,
DWORD dwNumDicts,
DWORD dwNumTypeSlots,
ClassLoader *pClassLoader,
BaseDomain *pDomain,
BOOL isIFace,
BOOL fHasGenericsStaticsInfo,
BOOL fNeedsRemotableMethodInfo,
BOOL fNeedsRemotingVtsInfo,
BOOL fHasThreadOrContextStatics
, AllocMemTracker *pamTracker
);
// Return total vtable slots : virtual, static, and instance method slots.
unsigned GetNumMethods()
{
LEAF_CONTRACT;
return m_wNumMethods;
}
void SetNumMethods(WORD wNumSlots)
{
LEAF_CONTRACT;
m_wNumMethods = wNumSlots;
}
//使用专门的数据结构来实现对Table中的可写部分的读写。而这些可写的部分,是定义在另外的一个专门的枚举类型的变量中。
PTR_MethodTableWriteableData m_pWriteableData;
//MethodTable类中最重要的一个定义,包涵了对EEClass的引用。
PTR_EEClass m_pEEClass;
//在寻找所谓的VTable定义的时候,着实费了一番功夫,最后得到一个结论:在sscli2.0中,去掉了m_Vtable[1]的显示定义。更多的是把Vtable当作一个Thunking Layer来在需要的时候操作。
//为此,MethodTalbe的定义中,还专门定义了一个struct(InteropMethodTableSlotData)来实现模拟就得Vtable结构对COM接口的访问。在需要使用到类似Vtable的时候直接计算某个Slot的Offset,并且使用专门的Accessor来进行访问。截取两个方法来说明这点:
inline PTR_SLOT GetVtable()
{
LEAF_CONTRACT;
return PTR_SLOT((PTR_HOST_TO_TADDR(this) + TADDR(GetVtableOffset())));
}
static inline DWORD GetVtableOffset()
{
LEAF_CONTRACT;
return (sizeof(MethodTable));
}
}
从下图中也可以很明显的看出MethodTable的结构以及其部分字段的表示的含义:
最后给出一个MethodTable中重要的数据结构:
struct ThreadAndContextStaticsBucket
{
// Offset which points to the TLS storage. Allocated lazily - -1 means no offset allocated yet.
DWORD m_dwThreadStaticsOffset;
// Offset which points to the CLS storage. Allocated lazily - -1 means no offset allocated yet.
DWORD m_dwContextStaticsOffset;
// Size of TLS fields
WORD m_wThreadStaticsSize;
// Size of CLS fields
WORD m_wContextStaticsSize;
};
typedef DPTR(ThreadAndContextStaticsBucket) PTR_ThreadAndContextStaticsBucket;
在上面的部分属性中,可以看到,一个Object的信息,并不是所有的都保存在MethodTalbe中的。这样分层设计的目的,还是一个字:效率。把一个Object使用的最频繁的部分,保持在MethodTable中,而使用的稍微不频繁的部分,保持到EEClass中,这部分信息,从名字上面就可以看出,主要是包涵了CLR执行引擎所需要的对一个Object的控制信息。也就是如下图所示,各个不同的部分所包涵的内容应用的重点和频率都不同:
接着,给出EEClass的一些重要的数据结构:
Class EEClass
{
//得到这个Object运行的一些基本环境。
BaseDomain * GetDomain();
Assembly * GetAssembly();
Module* GetLoaderModule();
Module* GetZapModule();
//point out the Module which contains this Object and its EEClass
PTR_Module m_pModule;
mdTypeDef m_cl;
//important property!point to the MethodTable of this Object.
PTR_MethodTable m_pMethodTable;
// NOTE: Place items that are WORD sized or smaller together,
//otherwise padding will be used implicitly by the C++ compiler
WORD m_wCCtorSlot;
WORD m_wDefaultCtorSlot;
BYTE m_NormType;
//number of instance Fields
WORD m_wNumInstanceFields;
//number of static fields
WORD m_wNumStaticFields;
WORD m_wNumHandleStatics;
WORD m_wNumBoxedStatics;
//implies the number of Object ref in a instance.
//GCDesc结构,也就是一个Object实现的时候的头部,Object Header可能会引用到,同时JIT也有可能引用到这个属性。
WORD m_wNumGCPointerSeries;
DWORD m_cbModuleDynamicID;
DWORD m_cbNonGCStaticFieldBytes;
DWORD m_dwNumInstanceFieldBytes;
//这个属性表面了对于一个Object里面的Fields,是在EEClass里面把每个Field对应的FieldDesc类连接在了一起。
FieldDesc *m_pFieldDescList;
DWORD m_dwAttrClass;
volatile DWORD m_VMFlags;
SecurityProperties m_SecProps;
//这里定义了MethodDescChunk,从最上面的图可以看到,从一个EEClass中是连接到MethodDescThunk链表上面的。比较重要的一个结构。
PTR_MethodDescChunk m_pChunks;
}
再回头看一下,MethodTable,m_Vtable(Vtable in MethodTable),MethodDescChunks这个Chain是如何连接起来的就十分清楚了:
最后,看看MethodDescChunk这个描述每一个Method的大块头的结构如何,这里,就不在分析一个MethodDescChunk里面究竟是如何实现的了:
MethodDescChunk的头部是上面的蓝色的部分。Prestub是中间的斜线的部分,而对每个方法的描述的部分,是上图的白色的区域,可以看到,包涵了这个方法的IL代码,SlotNumber,和一些标识位。每一行,是一个DWORD类型。
注意:m_op中,包涵了对这个方法的调用,是call JIT来进行实时编译,还是jump到一个已经编译好了的地方,m_op就是标识这两种不同的操作类型。而m_target里面,则是和m_op对应的内存地址。
Ok,写到这里,结合第一次讲System.Object的那篇文章,我想,对一个Object的认识应该有个大体的比较清晰的架构了。
最后,做个广告^_^
欢迎志同道合的同志们加入:Share Source CLI核心技术探索团队,这里,有一群对DotNet世界最核心技术充满好奇和执着探索的朋友们,期待你充满智慧的文章和加盟。