最近在看 C++ 類繼承中的欄位記憶體佈局,我就很好奇 C# 中的繼承鏈那些 private 欄位都哪裡去了? 在記憶體中是如何佈局的,畢竟在子類中是無法訪問的。 一:舉例說明 為了方便講述,先上一個例子: internal class Program { static void Main(string ...
最近在看 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}");
}
}
}
}
哈哈,是不是挺有意思。