SoFunction
Updated on 2025-03-07

Details of polymorphic underlying virtual method calls in C#

Preface:

In essence, CoreCLR is also written in C++, so it cannot escape the use of virtual tables to implement polymorphic gameplay, but the gameplay is a little more complicated. I hope this article will be helpful to everyone.

1. Polymorphic gameplay in C#

1. A simple C# example

For the sake of convenience, I define a Person class and a Chinese class, with the detailed code as follows:

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

            ();

            ();
        }
    }

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

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

2. Assembly code analysis

Next, use windbg to the next breakpoint at() and observe its disassembly code:

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

            ();

            ();
        }
    }

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

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

Judging from the assembly code, the logic is very clear, and the general steps are as follows:

(1)eax,dword ptr [ebp-8]

Get the first address of person on the heap from the stack (ebp-8). If you don’t believe it, you can try it with !do 027ea88c.

0:000> dp ebp-8 L1
0057f300  027ea88c
0:000> !do 027ea88c
Name:        
MethodTable: 05ce5d3c
EEClass:     05cd3380
Size:        12(0xc) bytes
File:        D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\
Fields:
None

(2)eax,dword ptr [eax]

If you know the memory layout of the instance on the heap, you should know that the first address stores the methodtable pointer, we can use !dumpmt 05ce5d3c to verify it.

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

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

(3)eax,dword ptr [eax+28h]

So what does this sentence mean? If you know CoreCLR, you should know that the methodedtable is hosted by a class MethodTable class, so it takes a field at the methodtable offset 0x28 position. So what is this offset field? Let’s first use dt to export the methodtable structure.

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]  " $ ("
   =7ad04c70 c_NonVirtualSlotsOffsets : [0]  " $ ($((, $ ("
   =7ad04c60 c_ModuleOverrideOffsets : [0]  " $ ($((,$((,(,,0 $ ($((, $ ("
   =7ad12838 c_OptionalMembersStartOffsets : [0]  "(((((((,(((,(,,0(((,(,,0(,,0,004"

Judging from the layout diagram of methodtable, eax+28h is the second field of the m_pMultipurposeSlot2 structure, because the first field is a virtual method table pointer. If you want to verify it, it is also very simple. Use !dumpmt -md 05ce5d3c to export all the methods, and then combine  dp 05ce5d3c to see if there are many methods after 0x5ce5d68.

0:000> !dumpmt -md 05ce5d3c
EEClass:         05cd3380
Module:          05addb14
Name:            
mdToken:         02000007
File:            D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\
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 ()
02610030 02605574   NONE ()
02610038 02605580   NONE ()
02610050 026055ac   NONE ()
05CF1CE0 05ce5d24   NONE ()
05CF1CE8 05ce5d30    JIT ..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

If you look at the output carefully, the 02610028 behind the 05ce5d68 above is the () method, and 02610030 corresponds to the () method.

(4)call dword ptr [eax+10h]

With the previous foundation, this sentence is easy to understand. It is to find the unit pointer position where SayHello is located from the m_pMultipurposeSlot2 structure, and then make a call 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

From the assembly point of view, it is still a piece of stake code. The implication is that the method has not been compiled by JIT. If the compilation is completed, the Entry (05CF1CE0) of 05CF1CE0 05ce5d24 NONE () here will also be modified synchronously. It is very simple to verify. We continue to go code to make it compile, and then dumpmt.

0:008> !dumpmt -md 05ce5d3c
EEClass:         05cd3380
Module:          05addb14
Name:            
mdToken:         02000007
File:            D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\
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 ()
02610030 02605574   NONE ()
02610038 02605580   NONE ()
02610050 026055ac   NONE ()
05CF2270 05ce5d24    JIT ()
05CF1CE8 05ce5d30    JIT ..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

At this time, you can see that it has changed from 05cf1ce0 to 05cf2270. This is the method code compiled by JIT. We use !U to decompile it.

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

D:\net6\ConsoleApplication2\ConsoleApp1\ @ 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\ @ 29:
05cf2286 8b0d74207e04    mov     ecx,dword ptr ds:[47E2074h] ("chinese")
05cf228c e8dffbffff      call    05cf1e70
05cf2291 90              nop

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

Finally, this is the method in polymorphism.

3. Summary

This is the end of this article about the details of the underlying virtual method calls of polymorphic in C#. For more related C# polymorphic content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!