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

在這篇文章我們將會(huì)通過閱讀CoreCLR的源代碼了解Object在內(nèi)存中的結(jié)構(gòu)和實(shí)際到內(nèi)存中瞧瞧Object

Object在內(nèi)存中的結(jié)構(gòu)

為了便于理解后面的內(nèi)容,我先用一張圖說明Object在內(nèi)存中的結(jié)構(gòu)

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

.Net中的Object包含了這三個(gè)部分

  • 指向頭部的指針

  • 指向類型信息的指針

  • 字段內(nèi)容

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

Object的源代碼解析

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

class Object
{
    PTR_MethodTable m_pMethTab;
}

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

typedef DPTR(class MethodTable) PTR_MethodTable;

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

這是獲取指向頭部的指針的函數(shù),我們可以看到這個(gè)指針剛好放在了Object的前面

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

這是獲取字段內(nèi)容的函數(shù),我們可以看到字段內(nèi)容剛好放在了Object的后面

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

我們可以看到Object中雖然只定義了指向類型信息的指針,但運(yùn)行時(shí)候前面會(huì)帶指向頭部的指針,并且后面會(huì)帶字段內(nèi)容
Object在內(nèi)存中擁有不定的長(zhǎng)度,并且起始地址是分配到的內(nèi)存地址+一個(gè)指針的大小
Object結(jié)構(gòu)比較特殊,所以這個(gè)對(duì)象的生成也需要特殊的處理,關(guān)于Object的生成我將在后面的篇幅中介紹

Object中定義的m_pMethTab還保存了額外的信息,因?yàn)檫@是一個(gè)指針值,所以總會(huì)以4或者8對(duì)齊,這樣最后兩個(gè)bit會(huì)總是為0
.Net利用了這兩個(gè)閑置的bit,分別用于保存GC Pinned和GC Marking,關(guān)于這里我也將在后面的篇幅中介紹

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是用于對(duì)齊的(讓m_SyncBlockValue在后面4位),值應(yīng)該為0
m_SyncBlockValue的前6位是標(biāo)記,后面26位是對(duì)應(yīng)的SyncBlockSyncBlockCache中的索引
SyncBlock的作用簡(jiǎn)單的來說就是用于線程同步的,例如下面的代碼會(huì)用到SyncBlock

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

ObjHeader只包含了SyncBlock,所以你可以看到有的講解Object結(jié)構(gòu)的文章中會(huì)用SyncBlock代替ObjHeader
關(guān)于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,這里省去介紹}

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

  • 類型的標(biāo)記,例如StaticsMask_DynamicStaticsMask_Generics等 (m_dwFlags)

    • 如果類型是字符串或數(shù)組還會(huì)保存每個(gè)元素的大小(ComponentSize),例如string是2 int[100]是4

  • 類型需要分配的內(nèi)存大小 (m_BaseSize)

  • 類型信息,例如有哪些成員和是否接口等等 (m_pCanonMT)

可以看出這個(gè)類型就是用于保存類型信息的,反射和動(dòng)態(tài)Cast都需要依賴它

實(shí)際查看內(nèi)存中的Object

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

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

這里我定義了MyClassMyStruct類型,先看Console.WriteLine(myClass)
這里把第一個(gè)參數(shù)設(shè)置到rcx并且調(diào)用Console.WriteLine函數(shù),為什么是rcx請(qǐng)看查看參考鏈接中對(duì)fastcall的介紹
rbp + 0x50 = 0x1fc8fde110

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

跳到內(nèi)存中以后可以看到選中的這8byte是指向?qū)ο蟮闹羔槪屛覀兝^續(xù)跳到0x1fcad88390

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

這里我們可以看到MyClass實(shí)例的真面目了,選中的8byte是指向MethodTable的指針
后面分別是指向StringMember的指針和IntMember的內(nèi)容
在這里指向ObjHeader的指針是一個(gè)空指針,這是正常的,微軟在代碼中有注釋This is often zero

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

這里是StringMember指向的內(nèi)容,分別是指向MethodTable的指針,字符串長(zhǎng)度和字符串內(nèi)容

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

這里是MyClassMethodTable,m_BaseSize是32
有興趣的可以去和MethodTable的成員一一對(duì)照,這里我就不跟下去了
讓我們?cè)倏聪聅truct是怎么處理的

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

可以看到只是簡(jiǎn)單的把值復(fù)制到了堆??臻g中(rbp是當(dāng)前frame的堆?;A(chǔ)地址)
讓我們?cè)賮砜聪?code style="line-height: 1.8; margin: 1px 5px; vertical-align: middle; display: inline-block; font-family: 'Courier New', sans-serif !important; font-size: 12px !important; border: 1px solid rgb(204, 204, 204) !important; padding: 0px 5px !important; border-radius: 3px !important; background-color: rgb(245, 245, 245) !important;">Console.WriteLine對(duì)于struct是怎么處理的,這里的處理相當(dāng)有趣

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

因?yàn)樾枰b箱,首先會(huì)要來一個(gè)箱子,箱子放在了rbp+30h

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

復(fù)制后,接下來把這個(gè)箱子傳給Console.WriteLine就和MyClass一樣了

另外再附一張實(shí)際查看ComponentSize的圖

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

彩蛋

看完了.Net中對(duì)Object的定義,讓我們?cè)倏聪翽ython中隊(duì)Object的定義
源代碼: https://github.com/python/cpython/blob/master/Include/object.h

#define PyObject_HEAD PyObject ob_base; // 每個(gè)子類都需要把這個(gè)放在最開頭typedef struct _object {#ifdef Py_TRACE_REFS
    struct _object *_ob_next; // Heap中的前一個(gè)對(duì)象
    struct _object *_ob_prev; // Heap中的后一個(gè)對(duì)象#endif
    Py_ssize_t ob_refcnt; // 引用計(jì)數(shù)
    struct _typeobject *ob_type; // 指向類型信息} PyObject;

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