CoreCLR源碼探索(一) Object是什么


.Net程序員們每天都在和Object在打交道
如果你問一個.Net程序員什么是Object,他可能會信誓旦旦的告訴你"Object還不簡單嗎,就是所有類型的基類"
這個答案是對的,但是不足以說明Object真正是什么

在這篇文章我們將會通過閱讀CoreCLR的源代碼了解Object在內存中的結構和實際到內存中瞧瞧Object

勘誤

通過更多的測試我發現了以下的錯誤,在此做出糾正,請之前的讀者見諒

  • Object前面的不是指向ObjHeader的指針而是ObjHeader自身
  • GC Pinned保存在ObjHeader(SyncBlock)而不是MethodTable

Object在內存中的結構

為了便於理解后面的內容,我先用一張圖說明Object在內存中的結構

.Net中的Object包含了這三個部分

  • 對象頭部(只包含了同步索引塊)
  • 指向類型信息的指針
  • 字段內容

微軟有一張更全的圖(說明的是.Net Framework的結構,但是基本和.Net Core一樣)

Object的源代碼解析

Object的定義(摘要)
源代碼: https://github.com/dotnet/coreclr/blob/master/src/vm/object.h

class Object
{
	PTR_MethodTable m_pMethTab;
}

PTR_MethodTable的定義,DPTR是一個指針的包裝類,你可以先理解為MethodTable*的等價
源代碼: https://github.com/dotnet/coreclr/blob/master/src/vm/common.h

typedef DPTR(class MethodTable) PTR_MethodTable;

在Object的定義中我們只看到了一個成員,這個成員就是指向類型信息的指針,那其他兩個部分呢?

這是獲取指向頭部的指針的函數,我們可以看到對象頭部剛好放在了Object的前面

PTR_ObjHeader GetHeader()
{
	LIMITED_METHOD_DAC_CONTRACT;
	return dac_cast<PTR_ObjHeader>(this) - 1;
}

這是獲取字段內容的函數,我們可以看到字段內容剛好放在了Object的后面

PTR_BYTE GetData(void)
{
	LIMITED_METHOD_CONTRACT;
	SUPPORTS_DAC;
	return dac_cast<PTR_BYTE>(this) + sizeof(Object);
}

我們可以看到Object中雖然只定義了指向類型信息的指針,但運行時候前面會帶對象頭部,並且后面會帶字段內容
Object結構比較特殊,所以這個對象的生成也需要特殊的處理,關於Object的生成我將在后面的篇幅中介紹

Object中定義的m_pMethTab還保存了額外的信息,因為這是一個指針值,所以總會以4或者8對齊,這樣最后兩個bit會總是為0
.Net利用了最后一個閑置的bit,用於保存GC Marking(GC標記對象是否存活),關於這里我也將在后面的篇幅中介紹

ObjHeader的源代碼解析

ObjHeader的定義(摘要)
源代碼: https://github.com/dotnet/coreclr/blob/master/src/vm/syncblk.h

class ObjHeader
{
// !!! Notice: m_SyncBlockValue *MUST* be the last field in ObjHeader.
#ifdef _WIN64
	DWORD    m_alignpad;
#endif // _WIN64
	
	Volatile<DWORD> m_SyncBlockValue;      // the Index and the Bits
}

m_alignpad是用於對齊的(讓m_SyncBlockValue在后面4位),值應該為0
m_SyncBlockValue的前6位是標記,后面26位是對應的SyncBlockSyncBlockCache中的索引
SyncBlock的作用簡單的來說就是用於線程同步的,例如下面的代碼會用到SyncBlock

var obj = new object();
lock (obj) { }

ObjHeader只包含了SyncBlock,所以你可以看到有的講解Object結構的文章中會用SyncBlock代替ObjHeader
關於SyncBlock更具體的講解還可以查看這篇文章

MethodTable的源代碼解析

MethodTable的定義(摘要)
源代碼: https://github.com/dotnet/coreclr/blob/master/src/vm/methodtable.h

class MethodTable
{
	// Low WORD is component size for array and string types (HasComponentSize() returns true).
	// Used for flags otherwise.
	DWORD m_dwFlags;
	
	// Base size of instance of this class when allocated on the heap
	DWORD m_BaseSize;
	
	WORD m_wFlags2;
	
	// Class token if it fits into 16-bits. If this is (WORD)-1, the class token is stored in the TokenOverflow optional member.
	WORD m_wToken;
	
	// <NICE> In the normal cases we shouldn't need a full word for each of these </NICE>
	WORD m_wNumVirtuals;
	WORD m_wNumInterfaces;
	
#ifdef _DEBUG
	LPCUTF8 debug_m_szClassName;
#endif //_DEBUG
	
	// Parent PTR_MethodTable if enum_flag_HasIndirectParent is not set. Pointer to indirection cell
	// if enum_flag_enum_flag_HasIndirectParent is set. The indirection is offset by offsetof(MethodTable, m_pParentMethodTable).
	// It allows casting helpers to go through parent chain natually. Casting helper do not need need the explicit check
	// for enum_flag_HasIndirectParentMethodTable.
	TADDR m_pParentMethodTable;
	
	PTR_Module m_pLoaderModule;    // LoaderModule. It is equal to the ZapModule in ngened images
	
	PTR_MethodTableWriteableData m_pWriteableData;
	
	union {
		EEClass *   m_pEEClass;
		TADDR       m_pCanonMT;
	};
	
	// m_pPerInstInfo and m_pInterfaceMap have to be at fixed offsets because of performance sensitive 
	// JITed code and JIT helpers. However, they are frequently not present. The space is used by other
	// multipurpose slots on first come first served basis if the fixed ones are not present. The other 
	// multipurpose are DispatchMapSlot, NonVirtualSlots, ModuleOverride (see enum_flag_MultipurposeSlotsMask).
	// The multipurpose slots that do not fit are stored after vtable slots.
	union
	{
		PTR_Dictionary *    m_pPerInstInfo;
		TADDR               m_ElementTypeHnd;
		TADDR               m_pMultipurposeSlot1;
	};
	
	union
	{
		InterfaceInfo_t *   m_pInterfaceMap;
		TADDR               m_pMultipurposeSlot2;
	};
	
	// 接下來還有一堆OPTIONAL_MEMBERS,這里省去介紹
}

這里的字段非常多,我將會在后面的篇幅一一講解,這里先說明MethodTable中大概有什么信息

  • 類型的標記,例如StaticsMask_DynamicStaticsMask_Generics等 (m_dwFlags)
    • 如果類型是字符串或數組還會保存每個元素的大小(ComponentSize),例如string是2 int[100]是4
  • 類型需要分配的內存大小 (m_BaseSize)
  • 類型信息,例如有哪些成員和是否接口等等 (m_pCanonMT)

可以看出這個類型就是用於保存類型信息的,反射和動態Cast都需要依賴它

實際查看內存中的Object

對Object的初步分析完了,可分析對了嗎?讓我們來實際檢查一下內存中Object是什么樣子的
VisualStudio有反編譯和查看內存的功能,如下圖

這里我定義了MyClassMyStruct類型,先看Console.WriteLine(myClass)
這里把第一個參數設置到rcx並且調用Console.WriteLine函數,為什么是rcx請看查看參考鏈接中對fastcall的介紹
rbp + 0x50 = 0x1fc8fde110

跳到內存中以后可以看到選中的這8byte是指向對象的指針,讓我們繼續跳到0x1fcad88390

這里我們可以看到MyClass實例的真面目了,選中的8byte是指向MethodTable的指針
后面分別是指向StringMember的指針和IntMember的內容
在這里ObjHeader中的同步索引塊值是0,這是正常的,微軟在代碼中有注釋This is often zero

這里是StringMember指向的內容,分別是指向MethodTable的指針,字符串長度和字符串內容

這里是MyClassMethodTablem_BaseSize是32
有興趣的可以去和MethodTable的成員一一對照,這里我就不跟下去了
讓我們再看下struct是怎么處理的

可以看到只是簡單的把值復制到了堆棧空間中(rbp是當前frame的堆棧基礎地址)
讓我們再來看下Console.WriteLine對於struct是怎么處理的,這里的處理相當有趣

因為需要裝箱,首先會要來一個箱子,箱子放在了rbp+30h

MyStruct中的值復制到了箱子中,rax+8的8是把值復制到MethodTable之后

復制后,接下來把這個箱子傳給Console.WriteLine就和MyClass一樣了

另外再附一張實際查看ComponentSize的圖

彩蛋

看完了.Net中對Object的定義,讓我們再看下Python中對Object的定義
源代碼: https://github.com/python/cpython/blob/master/Include/object.h

#define PyObject_HEAD PyObject ob_base; // 每個子類都需要把這個放在最開頭

typedef struct _object {
#ifdef Py_TRACE_REFS
	struct _object *_ob_next; // Heap中的前一個對象
	struct _object *_ob_prev; // Heap中的后一個對象
#endif
	Py_ssize_t ob_refcnt; // 引用計數
	struct _typeobject *ob_type; // 指向類型信息
} PyObject;

定義不一樣,但是作用還是類似的

參考

http://stackoverflow.com/questions/20033353/clr-implementation-of-virtual-method-calls-via-pointer-to-base-class
http://stackoverflow.com/questions/9808982/clr-implementation-of-virtual-method-calls-to-interface-members
http://stackoverflow.com/questions/1589669/overhead-of-a-net-array
https://en.wikipedia.org/wiki/X86_calling_conventions
https://github.com/dotnet/coreclr/blob/master/src/vm/object.inl
https://github.com/dotnet/coreclr/blob/master/src/vm/object.h
https://github.com/dotnet/coreclr/blob/master/src/vm/object.cpp
https://github.com/dotnet/coreclr/blob/master/src/vm/syncblk.h
https://github.com/dotnet/coreclr/blob/master/src/vm/syncblk.cpp
https://github.com/dotnet/coreclr/blob/master/src/vm/methodtable.inl
https://github.com/dotnet/coreclr/blob/master/src/vm/methodtable.h
https://github.com/dotnet/coreclr/blob/master/src/vm/methodtable.cpp
https://github.com/dotnet/coreclr/blob/master/src/vm/class.h
https://github.com/dotnet/coreclr/blob/master/src/inc/daccess.h
https://github.com/dotnet/coreclr/blob/master/src/debug/daccess/dacfn.cpp

寫在最后

因為是剛開始閱讀coreclr的代碼,如果有誤請在留言中指出
接下來有時間我將會着重閱讀和介紹這些內容

  • Object的生成和銷毀
  • Object繼承的原理(MethodTable)
  • Object同步的原理(ObjHeader, SyncBlock)
  • GC的工作方式
  • DACCESS

敬請期待

2017-11-03更新: 這個系列暫時會停止更新, 很可惜上面的3項不能介紹它們了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM