C# 類繼承中的私有欄位都去了哪裡?

2022-06-04 12:00:55

最近在看 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}");
                }
            }
        }
    }

哈哈,是不是挺有意思。

圖片名稱