最近在看 C++ 類繼承中的字段內存布局,我就很好奇 C# 中的繼承鏈那些 private 字段都哪里去了? 在內存中是如何布局的,畢竟在子類中是無法訪問的。
一:舉例說明
為了方便講述,先上一個例子:
internal?class?Program{static?void?Main(string[]?args){Chinese?chinese?=?new?Chinese();int?num?=?chinese.b;???//b?字段無法訪問,編譯報錯Console.WriteLine(num);}}public?class?Person{public?int?a?=?10;private?int?b?=?11;}public?class?Chinese?:?Person{public?int?c?=?12;}
根據 C# 的類繼承原則,上面的 chinese.b
寫法肯定是無法被編譯的,因為它屬于父類的 私有字段,既然無法被訪問,那這個 private b
到底去了哪里呢?要想找到答案,只能先從 chinese
實例處的匯編代碼看起,看看有沒有什么意外收獲。
二:查看 chinese 處匯編代碼
在 new chinese()
處下一個斷點,查看 Visual Stduio 2022
的反匯編窗口。

接下來我稍微解讀下:
1. 根據 MT 類型 實例化 chinese
07FD6176??mov?????????ecx,87205C4h??
07FD617B??call????????CORINFO_HELP_NEWSFAST?(06E30C0h)
這里的 87205C4h
就是 Chinese 類型的 MT,然后通過 CLR 下的 CORINFO_HELP_NEWSFAST
處的方法進行實例化。
2. 使用 chinese 的構造函數進行類初始化
07FD6180??mov?????????dword?ptr?[ebp-40h],eax??
07FD6183??mov?????????ecx,dword?ptr?[ebp-40h]??
07FD6186??call????????CLRStub[MethodDescPrestub]@7e34871e07fd5d20?(07FD5D20h)
07FD618B??mov?????????eax,dword?ptr?[ebp-40h]
這里的 eax 是 CORINFO_HELP_NEWSFAST
初始化方法的返回值,可以在 ecx,dword ptr [ebp-40h]
處下一個斷點,觀察它的內存布局。

從布局圖看,此時的 chinese 只是一個清零的默認狀態,此時的 a,b,c
三個字段還沒有被賦值,那什么時候被賦值呢?這就是構造函數要做的事情了,也就是上面的 CLRStub[MethodDescPrestub]@7e34871e07fd5d20 (07FD5D20h)
指令,接下來在 07FD618B
處下一個斷點,再次觀察 0x02C9F528
處的內存地址,也就是 ebp-40
的位置,接下來我們繼續執行,截圖如下:

從圖中可以看到,當構造函數執行完之后,有三處內存地址(變紅)被賦值了,依次是 a,b,c
,這時候是不是讓人眼前一亮。
3. 洞察真相
原來那個 b=11
并沒有丟,而是被 chinese
類給完全繼承下來的,而且布局規則是 父類
字段在前, 子類
字段在后的一種方式,有點意思,接下來的問題是如何把它提取出來?
三:如何提取 b 字段
如果是 C 語言,我們用 *(pointer+2)
就可以輕松提取,那用托管的 C# 如何去實現呢? 可以用復雜的 Marshal
包裝類,應該也可以變相的使用 Span
去搞定,這里我就不麻煩了,直接用非安全代碼下的 指針
去擺平,在 a
字段偏移 +4 的位置上提取, 參考代碼如下:
static?void?Main(string[]?args){unsafe{Chinese?chinese?=?new?Chinese();fixed?(int*?ch?=?&chinese.a){int?b?=?*(ch?+?1);Console.WriteLine($"b={b}");}}}}
哈哈,是不是挺有意思。
