聊聊 C# 中的多態底層 (虛方法調用) 是怎麼玩的

来源:https://www.cnblogs.com/huangxincheng/archive/2022/06/01/16333775.html
-Advertisement-
Play Games

最近在看 C++ 的虛方法調用實現原理,大概就是說在 class 的首位置存放著一個指向 vtable array 指針數組 的指針,而 vtable array 中的每一個指針元素指向的就是各自的 虛方法,實現方式很有意思,哈哈,現在我很好奇 C# 中如何實現的。 一: C# 中的多態玩法 1. ...


最近在看 C++ 的虛方法調用實現原理,大概就是說在 class 的首位置存放著一個指向 vtable array 指針數組 的指針,而 vtable array 中的每一個指針元素指向的就是各自的 虛方法,實現方式很有意思,哈哈,現在我很好奇 C# 中如何實現的。

一: C# 中的多態玩法

1. 一個簡單的 C# 例子

為了方便說明,我就定義一個 Person 類和一個 Chinese 類,詳細代碼如下:


    internal class Program
    {
        static void Main(string[] args)
        {
            Person person = new Chinese();

            person.SayHello();

            Console.ReadLine();
        }
    }

    public class Person
    {
        public virtual void SayHello()
        {
            Console.WriteLine("sayhello");
        }
    }

    public class Chinese: Person
    {
        public override void SayHello()
        {
            Console.WriteLine("chinese");
        }
    }
}

2. 彙編代碼分析

接下來用 windbg 在 person.SayHello() 處下一個斷點,觀察一下它的反彙編代碼:


D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 9:
05cf21b3 b93c5dce05      mov     ecx,5CE5D3Ch (MT: ConsoleApp1.Chinese)
05cf21b8 e8030f89fa      call    005830c0 (JitHelp: CORINFO_HELP_NEWSFAST)
05cf21bd 8945f4          mov     dword ptr [ebp-0Ch],eax
05cf21c0 8b4df4          mov     ecx,dword ptr [ebp-0Ch]
05cf21c3 e820fbffff      call    05cf1ce8 (ConsoleApp1.Chinese..ctor(), mdToken: 0600000A)
05cf21c8 8b4df4          mov     ecx,dword ptr [ebp-0Ch]
05cf21cb 894df8          mov     dword ptr [ebp-8],ecx

D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 11:
>>> 05cf21ce 8b4df8          mov     ecx,dword ptr [ebp-8]
05cf21d1 8b45f8          mov     eax,dword ptr [ebp-8]
05cf21d4 8b00            mov     eax,dword ptr [eax]
05cf21d6 8b4028          mov     eax,dword ptr [eax+28h]
05cf21d9 ff5010          call    dword ptr [eax+10h]
05cf21dc 90              nop

從彙編代碼看,邏輯非常清晰,大體步驟如下:

  1. eax,dword ptr [ebp-8]

從棧上(ebp-8)處獲取 person 在堆上的首地址,如果不相信的話,可以用 !do 027ea88c 試試看。


0:000> dp ebp-8 L1
0057f300  027ea88c

0:000> !do 027ea88c
Name:        ConsoleApp1.Chinese
MethodTable: 05ce5d3c
EEClass:     05cd3380
Size:        12(0xc) bytes
File:        D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dll
Fields:
None

  1. eax,dword ptr [eax]

如果大家瞭解 實例 在堆上的記憶體佈局的話,應該知道,這個首地址存放的就是 methodtable 指針,我們可以用 !dumpmt 05ce5d3c 來驗證下。


0:000> dp 027ea88c L1
027ea88c  05ce5d3c

0:000> !dumpmt 05ce5d3c
EEClass:         05cd3380
Module:          05addb14
Name:            ConsoleApp1.Chinese
mdToken:         02000007
File:            D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dll
BaseSize:        0xc
ComponentSize:   0x0
DynamicStatics:  false
ContainsPointers false
Slots in VTable: 6
Number of IFaces in IFaceMap: 0

  1. eax,dword ptr [eax+28h]

那這句話是什麼意思呢?如果你瞭解 CoreCLR 的話,你應該知道 methedtable 是由一個 class MethodTable 類來承載的,所以它取了 methodtable 偏移 0x28 位置的一個欄位,那這個偏移欄位是什麼呢? 我們先用 dt 把 methodtable 結構給導出來。


0:000> dt 05ce5d3c MethodTable
coreclr!MethodTable
   =7ad96bc8 s_pMethodDataCache : 0x00639ec8 MethodDataCache
   =7ad96bc4 s_fUseParentMethodData : 0n1
   =7ad96bcc s_fUseMethodDataCache : 0n1
   +0x000 m_dwFlags        : 0xc
   +0x004 m_BaseSize       : 0x74088
   +0x008 m_wFlags2        : 5
   +0x00a m_wToken         : 0
   +0x00c m_wNumVirtuals   : 0x5ccc
   +0x00e m_wNumInterfaces : 0x5ce
   +0x010 m_pParentMethodTable : IndirectPointer<MethodTable *>
   +0x014 m_pLoaderModule  : PlainPointer<Module *>
   +0x018 m_pWriteableData : PlainPointer<MethodTableWriteableData *>
   +0x01c m_pEEClass       : PlainPointer<EEClass *>
   +0x01c m_pCanonMT       : PlainPointer<unsigned long>
   +0x020 m_pPerInstInfo   : PlainPointer<PlainPointer<Dictionary *> *>
   +0x020 m_ElementTypeHnd : 0
   +0x020 m_pMultipurposeSlot1 : 0
   +0x024 m_pInterfaceMap  : PlainPointer<InterfaceInfo_t *>
   +0x024 m_pMultipurposeSlot2 : 0x5ce5d68
   =7ad04c78 c_DispatchMapSlotOffsets : [0]  " $ (System.Private.CoreLib.dll"
   =7ad04c70 c_NonVirtualSlotsOffsets : [0]  " $ ($((, $ (System.Private.CoreLib.dll"
   =7ad04c60 c_ModuleOverrideOffsets : [0]  " $ ($((,$((,(,,0 $ ($((, $ (System.Private.CoreLib.dll"
   =7ad12838 c_OptionalMembersStartOffsets : [0]  "(((((((,(((,(,,0(((,(,,0(,,0,004"

從 methodtable 的佈局圖來看, eax+28hm_pMultipurposeSlot2 結構的第二個欄位了,因為第一個欄位是 虛方法表指針,如果要驗證的話,也很簡單,用 !dumpmt -md 05ce5d3c 把所有的方法給導出來,然後結合 dp 05ce5d3c 看下 0x5ce5d68 之後是不是許多的方法。


0:000> !dumpmt -md 05ce5d3c
EEClass:         05cd3380
Module:          05addb14
Name:            ConsoleApp1.Chinese
mdToken:         02000007
File:            D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dll
BaseSize:        0xc
ComponentSize:   0x0
DynamicStatics:  false
ContainsPointers false
Slots in VTable: 6
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
   Entry MethodDe    JIT Name
02610028 02605568   NONE System.Object.Finalize()
02610030 02605574   NONE System.Object.ToString()
02610038 02605580   NONE System.Object.Equals(System.Object)
02610050 026055ac   NONE System.Object.GetHashCode()
05CF1CE0 05ce5d24   NONE ConsoleApp1.Chinese.SayHello()
05CF1CE8 05ce5d30    JIT ConsoleApp1.Chinese..ctor()
0:000> dp 05ce5d3c L10
05ce5d3c  00000200 0000000c 00074088 00000005
05ce5d4c  05ce5ccc 05addb14 05ce5d7c 05cd3380
05ce5d5c  05cf1ce8 00000000 05ce5d68 02610028
05ce5d6c  02610030 02610038 02610050 05cf1ce0

仔細看輸出,上面的 05ce5d68 後面的 02610028 就是 System.Object.Finalize() 方法,02610030 對應著 System.Object.ToString() 方法。

  1. call dword ptr [eax+10h]

有了前面的基礎,這句話就好理解了,它是從 m_pMultipurposeSlot2 結構中找 SayHello 所在的單元指針位置,然後做 call 調用。


0:000> !U 05cf1ce0
Unmanaged code
05cf1ce0 e88f9dde74      call    coreclr!PrecodeFixupThunk (7aadba74)
05cf1ce5 5e              pop     esi
05cf1ce6 0001            add     byte ptr [ecx],al
05cf1ce8 e913050000      jmp     05cf2200
05cf1ced 5f              pop     edi
05cf1cee 0300            add     eax,dword ptr [eax]
05cf1cf0 245d            and     al,5Dh
05cf1cf2 ce              into
05cf1cf3 0500000000      add     eax,0
05cf1cf8 0000            add     byte ptr [eax],al

從彙編看,它還是一段 樁代碼,言外之意就是該方法沒有被 JIT 編譯,如果編譯完了,這裡的 05CF1CE0 05ce5d24 NONE ConsoleApp1.Chinese.SayHello() 的 Entry (05CF1CE0) 也會被同步修改,驗證一下很簡單,我們繼續 go 代碼讓其編譯完成,然後再 dumpmt 。


0:008> !dumpmt -md 05ce5d3c
EEClass:         05cd3380
Module:          05addb14
Name:            ConsoleApp1.Chinese
mdToken:         02000007
File:            D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dll
BaseSize:        0xc
ComponentSize:   0x0
DynamicStatics:  false
ContainsPointers false
Slots in VTable: 6
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
   Entry MethodDe    JIT Name
02610028 02605568   NONE System.Object.Finalize()
02610030 02605574   NONE System.Object.ToString()
02610038 02605580   NONE System.Object.Equals(System.Object)
02610050 026055ac   NONE System.Object.GetHashCode()
05CF2270 05ce5d24    JIT ConsoleApp1.Chinese.SayHello()
05CF1CE8 05ce5d30    JIT ConsoleApp1.Chinese..ctor()

0:008> dp 05ce5d3c L10
05ce5d3c  00000200 0000000c 00074088 00000005
05ce5d4c  05ce5ccc 05addb14 05ce5d7c 05cd3380
05ce5d5c  05cf1ce8 00000000 05ce5d68 02610028
05ce5d6c  02610030 02610038 02610050 05cf2270

此時可以看到它由 05cf1ce0 變成了 05cf2270, 這個就是 JIT 編譯後的方法代碼,我們用 !U 反編譯下。


0:008> !U 05cf2270
Normal JIT generated code
ConsoleApp1.Chinese.SayHello()
ilAddr is 05E720D5 pImport is 008F6E88
Begin 05CF2270, size 27

D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 28:
>>> 05cf2270 55              push    ebp
05cf2271 8bec            mov     ebp,esp
05cf2273 50              push    eax
05cf2274 894dfc          mov     dword ptr [ebp-4],ecx
05cf2277 833d74dcad0500  cmp     dword ptr ds:[5ADDC74h],0
05cf227e 7405            je      05cf2285
05cf2280 e8cb2bf174      call    coreclr!JIT_DbgIsJustMyCode (7ac04e50)
05cf2285 90              nop

D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 29:
05cf2286 8b0d74207e04    mov     ecx,dword ptr ds:[47E2074h] ("chinese")
05cf228c e8dffbffff      call    05cf1e70
05cf2291 90              nop

D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 30:
05cf2292 90              nop
05cf2293 8be5            mov     esp,ebp
05cf2295 5d              pop     ebp
05cf2296 c3              ret

終於這就是多態下的 ConsoleApp1.Chinese.SayHello 方法啦。

3. 總結

本質上來說,CoreCLR 也是 C++ 寫的,所以也逃不過用 虛表 來實現多態的玩法, 不過玩法也稍微複雜了一些,希望本篇對大家有幫助。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Redis預減庫存 主要思路減少對資料庫的訪問,之前的減庫存,直接訪問資料庫,讀取庫存,當高併發請求到來的時候,大量的讀取數據有可能會導致資料庫的崩潰。 思路: 系統初始化的時候,將商品庫存載入到Redis 緩存中保存 收到請求的時候,現在Redis中拿到該商品的庫存值,進行庫存預減,如果減完之後庫 ...
  • 2018年2月28日Spring Boot進入2.0時代,距今已經超過4年了。 2022 年 11 月 Spring Boot 3.0 將正式發佈,它將基於 Spring Framework 6.0,並且需要 Java 17 或更高版本,同時它也將是Jakarta EE 9的第一個 Spring B ...
  • 快速安裝 pip install matplotlib 折線圖 快速入門 import matplotlib.pyplot as plt import random x=range(10) # 定義x軸的數據 y=[random.uniform(15,35) for i in x] # 定義y軸的數 ...
  • 一、日誌文件輸出說明 日誌目錄: /nchome/nclogs/servername/ ,其中servername集群時目錄類似為master,ncMem01等。非集群時目錄為:server1(服務名) 模塊 輸出格式 說明 anonymous anony-log.log 業務日誌,如果沒有配置模塊 ...
  • 作者:代碼的色彩 鏈接:https://juejin.cn/post/7062662600437268493 1.前言 你是否對大廠展示的五花八門,花花綠綠的架構設計圖所深深吸引,當我們想用幾張圖來介紹下業務系統,是不是對著畫布不知從何下手?作為技術扛把子的筒子們是不是需要一張圖來描述系統,讓系統各 ...
  • 概念 棧(stack)是一種運算受限的線性表。棧只能從末尾插入或刪除數據。我們把這一端稱為棧頂,對應地,把另一端稱為棧底。 隊列(queue)是一種線性表。它允許在表的某一端進行插入操作,在另一端進行刪除操作。我們把進行刪除操作的一端稱作隊列的隊尾,把進行插入操作的一端稱作隊列的隊首。 實現 註:由 ...
  • Liunx安裝Nacos 一,準備安裝包 github下載點 同時請確認Linux已安裝jdk 二,在/usr/local/目錄下創建一個文件夾用於上傳和解壓Nacos cd /usr/local/ #這裡創建文件夾名字可隨意,解壓後會生成一個名為nacos的文件夾,後續會移動至/usr/local ...
  • 最近通過WPF開發項目,為了對WPF知識點進行總結,所以利用業餘時間,開發一個學生信息管理系統【Student Information Management System】。前三篇文章進行了框架搭建和模塊劃分,後臺WebApi介面編寫,以及課程管理模塊開發,本文在前三篇基礎之上,繼續深入開發學生信息... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...